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

@@ -1,35 +1,50 @@
from sqlalchemy import Column, Integer, String, DateTime, JSON, ForeignKey, Float, Table, Text
from sqlalchemy import (
Boolean,
Column,
Integer,
String,
DateTime,
JSON,
ForeignKey,
Float,
Table,
Text,
)
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base
# Association Table for Many-to-Many Relationship between Track and Artist
track_artists = Table(
'track_artists',
"track_artists",
Base.metadata,
Column('track_id', String, ForeignKey('tracks.id'), primary_key=True),
Column('artist_id', String, ForeignKey('artists.id'), primary_key=True)
Column("track_id", String, ForeignKey("tracks.id"), primary_key=True),
Column("artist_id", String, ForeignKey("artists.id"), primary_key=True),
)
class Artist(Base):
__tablename__ = "artists"
id = Column(String, primary_key=True, index=True) # Spotify ID
id = Column(String, primary_key=True, index=True) # Spotify ID
name = Column(String)
genres = Column(JSON, nullable=True) # List of genre strings
image_url = Column(String, nullable=True) # Artist profile image
genres = Column(JSON, nullable=True) # List of genre strings
image_url = Column(String, nullable=True) # Artist profile image
# Relationships
tracks = relationship("Track", secondary=track_artists, back_populates="artists")
class Track(Base):
__tablename__ = "tracks"
id = Column(String, primary_key=True, index=True) # Spotify ID
id = Column(String, primary_key=True, index=True) # Spotify ID
name = Column(String)
artist = Column(String) # Display string (e.g. "Drake, Future") - kept for convenience
artist = Column(
String
) # Display string (e.g. "Drake, Future") - kept for convenience
album = Column(String)
image_url = Column(String, nullable=True) # Album art
image_url = Column(String, nullable=True) # Album art
duration_ms = Column(Integer)
popularity = Column(Integer, nullable=True)
@@ -55,7 +70,7 @@ class Track(Base):
genres = Column(JSON, nullable=True)
# AI Analysis fields
lyrics = Column(Text, nullable=True) # Full lyrics from Genius
lyrics = Column(Text, nullable=True) # Full lyrics from Genius
lyrics_summary = Column(String, nullable=True)
genre_tags = Column(String, nullable=True)
@@ -71,11 +86,13 @@ class PlayHistory(Base):
id = Column(Integer, primary_key=True, index=True)
track_id = Column(String, ForeignKey("tracks.id"))
played_at = Column(DateTime, index=True) # The timestamp from Spotify
# Context (album, playlist, etc.)
played_at = Column(DateTime, index=True)
context_uri = Column(String, nullable=True)
listened_ms = Column(Integer, nullable=True)
skipped = Column(Boolean, nullable=True)
source = Column(String, nullable=True)
track = relationship("Track", back_populates="plays")
@@ -84,16 +101,19 @@ class AnalysisSnapshot(Base):
Stores the computed statistics and LLM analysis for a given period.
Allows for trend analysis over time.
"""
__tablename__ = "analysis_snapshots"
id = Column(Integer, primary_key=True, index=True)
date = Column(DateTime, default=datetime.utcnow, index=True) # When the analysis was run
date = Column(
DateTime, default=datetime.utcnow, index=True
) # When the analysis was run
period_start = Column(DateTime)
period_end = Column(DateTime)
period_label = Column(String) # e.g., "last_30_days", "monthly_nov_2023"
period_label = Column(String) # e.g., "last_30_days", "monthly_nov_2023"
# The heavy lifting: stored as JSON blobs
metrics_payload = Column(JSON) # The input to the LLM (StatsService output)
narrative_report = Column(JSON) # The output from the LLM (NarrativeService output)
metrics_payload = Column(JSON) # The input to the LLM (StatsService output)
narrative_report = Column(JSON) # The output from the LLM (NarrativeService output)
model_used = Column(String, nullable=True) # e.g. "gemini-1.5-flash"
model_used = Column(String, nullable=True) # e.g. "gemini-1.5-flash"