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:
bnair123
2025-12-30 00:15:01 +04:00
parent faee830545
commit 887e78bf47
26 changed files with 1942 additions and 662 deletions

View File

@@ -5,37 +5,38 @@ const NarrativeSection = ({ narrative, vibe }) => {
if (!narrative) return null;
const persona = narrative.persona || "THE UNKNOWN LISTENER";
const vibeCheck = narrative.vibe_check || "Analyzing auditory aura...";
const vibeCheckShort = narrative.vibe_check_short || narrative.vibe_check?.substring(0, 120) + "..." || "Analyzing auditory aura...";
// Generate tags based on vibe metrics if available
const getTags = () => {
if (!vibe) return [];
const tags = [];
if (vibe.valence > 0.6) tags.push({ text: "HIGH VALENCE", color: "primary" });
else if (vibe.valence < 0.4) tags.push({ text: "MELANCHOLIC", color: "accent-purple" });
const valence = vibe.valence || 0;
const energy = vibe.energy || 0;
const danceability = vibe.danceability || 0;
if (valence > 0.6) tags.push({ text: "HIGH VALENCE", color: "primary" });
else if (valence < 0.4) tags.push({ text: "MELANCHOLIC", color: "accent-purple" });
if (vibe.energy > 0.6) tags.push({ text: "HIGH ENERGY", color: "accent-neon" });
else if (vibe.energy < 0.4) tags.push({ text: "CHILL VIBES", color: "accent-purple" });
if (energy > 0.6) tags.push({ text: "HIGH ENERGY", color: "accent-neon" });
else if (energy < 0.4) tags.push({ text: "CHILL VIBES", color: "accent-purple" });
if (vibe.danceability > 0.7) tags.push({ text: "DANCEABLE", color: "primary" });
if (danceability > 0.7) tags.push({ text: "DANCEABLE", color: "primary" });
return tags.slice(0, 3); // Max 3 tags
return tags.slice(0, 3);
};
const tags = getTags();
// Default tags if none generated
if (tags.length === 0) {
tags.push({ text: "ECLECTIC", color: "primary" });
tags.push({ text: "MYSTERIOUS", color: "accent-purple" });
}
return (
<section className="relative rounded-2xl overflow-hidden min-h-[400px] flex items-center justify-center p-8 bg-card-dark border border-[#222f49]">
{/* Dynamic Background */}
<section className="relative rounded-2xl overflow-hidden min-h-[300px] flex items-center justify-center p-8 bg-card-dark border border-[#222f49]">
<div className="absolute inset-0 mood-gradient"></div>
<div className="relative z-10 flex flex-col items-center text-center max-w-2xl gap-6">
<div className="relative z-10 flex flex-col items-center text-center max-w-2xl gap-4">
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
@@ -47,11 +48,11 @@ const NarrativeSection = ({ narrative, vibe }) => {
</h1>
</motion.div>
<div className="font-mono text-primary/80 text-lg md:text-xl font-medium tracking-wide">
<span className="typing-cursor">{vibeCheck}</span>
<div className="font-mono text-primary/80 text-base md:text-lg font-medium tracking-wide max-w-lg">
<span className="typing-cursor">{vibeCheckShort}</span>
</div>
<div className="mt-4 flex gap-3 flex-wrap justify-center">
<div className="mt-2 flex gap-3 flex-wrap justify-center">
{tags.map((tag, i) => (
<span key={i} className={`px-3 py-1 rounded-full text-xs font-bold bg-${tag.color}/20 text-${tag.color} border border-${tag.color}/20`}>
{tag.text}