mirror of
https://github.com/bnair123/MusicAnalyser.git
synced 2026-02-25 11:46:07 +00:00
Add skip tracking, compressed heatmap, listening log, docs, tests, and OpenAI support
Major changes: - Add skip tracking: poll currently-playing every 15s, detect skips (<30s listened) - Add listening-log and sessions API endpoints - Fix ReccoBeats client to extract spotify_id from href response - Compress heatmap from 24 hours to 6 x 4-hour blocks - Add OpenAI support in narrative service (use max_completion_tokens for new models) - Add ListeningLog component with timeline and list views - Update all frontend components to use real data (album art, play counts) - Add docker-compose external network (dockernet) support - Add comprehensive documentation (API, DATA_MODEL, ARCHITECTURE, FRONTEND) - Add unit tests for ingest and API endpoints
This commit is contained in:
@@ -4,27 +4,21 @@ const StatsGrid = ({ metrics }) => {
|
||||
if (!metrics) return null;
|
||||
|
||||
const totalMinutes = Math.round((metrics.volume?.estimated_minutes || 0));
|
||||
// Calculate days for the "That's X days straight" text
|
||||
const daysListened = (totalMinutes / (24 * 60)).toFixed(1);
|
||||
|
||||
const obsessionTrack = metrics.volume?.top_tracks?.[0];
|
||||
const obsessionName = obsessionTrack ? obsessionTrack.name : "N/A";
|
||||
const obsessionArtist = obsessionTrack ? obsessionTrack.artist : "N/A";
|
||||
const obsessionCount = obsessionTrack ? obsessionTrack.count : 0;
|
||||
|
||||
// Fallback image if we don't have one (API currently doesn't seem to return it in top_tracks simple list)
|
||||
// We'll use a nice gradient or abstract pattern
|
||||
const obsessionImage = "https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=2070&auto=format&fit=crop";
|
||||
const obsessionImage = obsessionTrack?.image || "https://images.unsplash.com/photo-1614613535308-eb5fbd3d2c17?q=80&w=400&auto=format&fit=crop";
|
||||
|
||||
const newDiscoveries = metrics.volume?.unique_artists || 0;
|
||||
const uniqueArtists = metrics.volume?.unique_artists || 0;
|
||||
|
||||
// Mocking the "Underground" percentage for now as it's not in the standard payload
|
||||
// Could derive from popularity if available, but let's randomize slightly based on unique artists to make it feel dynamic
|
||||
const undergroundScore = Math.min(95, Math.max(10, Math.round((newDiscoveries % 100))));
|
||||
const hipsterScore = metrics.taste?.hipster_score || 0;
|
||||
const obscurityRating = metrics.taste?.obscurity_rating || 0;
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Card 1: Minutes Listened */}
|
||||
<div className="bg-card-dark border border-[#222f49] rounded-xl p-6 flex flex-col justify-between h-full min-h-[200px] group hover:border-primary/50 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<span className="text-slate-400 text-sm font-medium uppercase tracking-wider">Minutes Listened</span>
|
||||
@@ -39,7 +33,6 @@ const StatsGrid = ({ metrics }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Obsession Track */}
|
||||
<div className="bg-card-dark border border-[#222f49] rounded-xl relative overflow-hidden h-full min-h-[200px] group lg:col-span-2">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105"
|
||||
@@ -62,16 +55,13 @@ const StatsGrid = ({ metrics }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 3: New Discoveries & Mainstream Gauge */}
|
||||
<div className="flex flex-col gap-4 h-full">
|
||||
{/* Discoveries */}
|
||||
<div className="bg-card-dark border border-[#222f49] rounded-xl p-5 flex-1 flex flex-col justify-center items-center text-center">
|
||||
<span className="material-symbols-outlined text-4xl text-primary mb-2">visibility</span>
|
||||
<div className="text-3xl font-bold text-white">{newDiscoveries}</div>
|
||||
<div className="text-3xl font-bold text-white">{uniqueArtists}</div>
|
||||
<div className="text-slate-400 text-xs uppercase tracking-wider">Unique Artists</div>
|
||||
</div>
|
||||
|
||||
{/* Gauge */}
|
||||
<div className="bg-card-dark border border-[#222f49] rounded-xl p-5 flex-1 flex flex-col justify-center items-center">
|
||||
<div className="relative size-20">
|
||||
<svg className="size-full -rotate-90" viewBox="0 0 36 36">
|
||||
@@ -81,15 +71,16 @@ const StatsGrid = ({ metrics }) => {
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeDasharray={`${undergroundScore}, 100`}
|
||||
strokeDasharray={`${Math.min(hipsterScore, 100)}, 100`}
|
||||
strokeWidth="3"
|
||||
></path>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex items-center justify-center flex-col">
|
||||
<span className="text-sm font-bold text-white">{undergroundScore}%</span>
|
||||
<span className="text-sm font-bold text-white">{hipsterScore.toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-slate-400 text-[10px] uppercase tracking-wider mt-2">Underground Certified</div>
|
||||
<div className="text-slate-400 text-[10px] uppercase tracking-wider mt-2">Hipster Score</div>
|
||||
<div className="text-slate-500 text-[9px] mt-1">Obscurity: {obscurityRating.toFixed(0)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user