feat: migrate to PostgreSQL and enhance playlist curation

- Migrate database from SQLite to PostgreSQL (100.91.248.114:5433)
- Fix playlist curation to use actual top tracks instead of AI name matching
- Add /playlists/history endpoint for historical playlist viewing
- Add Playlist Archives section to frontend with expandable history
- Add playlist-modify-* scopes to Spotify OAuth for playlist creation
- Rewrite Genius client to use official API (fixes 403 scraping blocks)
- Ensure playlists are created on Spotify before curation attempts
- Add DATABASE.md documentation for PostgreSQL schema
- Add migrations for PlaylistConfig and composition storage
This commit is contained in:
bnair123
2025-12-30 22:24:56 +04:00
parent 26b4895695
commit 272148c5bf
19 changed files with 1130 additions and 145 deletions

View File

@@ -10,6 +10,7 @@ from .models import (
PlayHistory as PlayHistoryModel,
Track as TrackModel,
AnalysisSnapshot,
PlaylistConfig,
)
from . import schemas
from .ingest import (
@@ -220,13 +221,18 @@ async def refresh_six_hour_playlist(db: Session = Depends(get_db)):
end_date = datetime.utcnow()
start_date = end_date - timedelta(hours=6)
spotify_client = get_spotify_client()
playlist_service = PlaylistService(
db=db,
spotify_client=get_spotify_client(),
spotify_client=spotify_client,
recco_client=get_reccobeats_client(),
narrative_service=NarrativeService(),
)
# Ensure playlists exist (creates on Spotify if needed)
user_id = await spotify_client.get_current_user_id()
await playlist_service.ensure_playlists_exist(user_id)
result = await playlist_service.curate_six_hour_playlist(start_date, end_date)
snapshot = AnalysisSnapshot(
@@ -239,6 +245,7 @@ async def refresh_six_hour_playlist(db: Session = Depends(get_db)):
playlist_theme=result.get("theme_name"),
playlist_theme_reasoning=result.get("description"),
six_hour_playlist_id=result.get("playlist_id"),
playlist_composition=result.get("composition"),
)
db.add(snapshot)
db.commit()
@@ -256,13 +263,18 @@ async def refresh_daily_playlist(db: Session = Depends(get_db)):
end_date = datetime.utcnow()
start_date = end_date - timedelta(days=1)
spotify_client = get_spotify_client()
playlist_service = PlaylistService(
db=db,
spotify_client=get_spotify_client(),
spotify_client=spotify_client,
recco_client=get_reccobeats_client(),
narrative_service=NarrativeService(),
)
# Ensure playlists exist (creates on Spotify if needed)
user_id = await spotify_client.get_current_user_id()
await playlist_service.ensure_playlists_exist(user_id)
result = await playlist_service.curate_daily_playlist(start_date, end_date)
snapshot = AnalysisSnapshot(
@@ -273,6 +285,7 @@ async def refresh_daily_playlist(db: Session = Depends(get_db)):
metrics_payload={},
narrative_report={},
daily_playlist_id=result.get("playlist_id"),
playlist_composition=result.get("composition"),
)
db.add(snapshot)
db.commit()
@@ -286,32 +299,71 @@ async def refresh_daily_playlist(db: Session = Depends(get_db)):
@app.get("/playlists")
async def get_playlists_metadata(db: Session = Depends(get_db)):
"""Returns metadata for the managed playlists."""
latest_snapshot = (
db.query(AnalysisSnapshot)
.filter(AnalysisSnapshot.six_hour_playlist_id != None)
.order_by(AnalysisSnapshot.date.desc())
.first()
six_hour_config = (
db.query(PlaylistConfig).filter(PlaylistConfig.key == "six_hour").first()
)
daily_config = (
db.query(PlaylistConfig).filter(PlaylistConfig.key == "daily").first()
)
return {
"six_hour": {
"id": latest_snapshot.six_hour_playlist_id
if latest_snapshot
"id": six_hour_config.spotify_id
if six_hour_config
else os.getenv("SIX_HOUR_PLAYLIST_ID"),
"theme": latest_snapshot.playlist_theme if latest_snapshot else "N/A",
"reasoning": latest_snapshot.playlist_theme_reasoning
if latest_snapshot
else "N/A",
"last_refresh": latest_snapshot.date.isoformat()
if latest_snapshot
"theme": six_hour_config.current_theme if six_hour_config else "N/A",
"reasoning": six_hour_config.description if six_hour_config else "N/A",
"last_refresh": six_hour_config.last_updated.isoformat()
if six_hour_config
else None,
"composition": six_hour_config.composition if six_hour_config else [],
},
"daily": {
"id": latest_snapshot.daily_playlist_id
if latest_snapshot
"id": daily_config.spotify_id
if daily_config
else os.getenv("DAILY_PLAYLIST_ID"),
"last_refresh": latest_snapshot.date.isoformat()
if latest_snapshot
"theme": daily_config.current_theme if daily_config else "N/A",
"reasoning": daily_config.description if daily_config else "N/A",
"last_refresh": daily_config.last_updated.isoformat()
if daily_config
else None,
"composition": daily_config.composition if daily_config else [],
},
}
@app.get("/playlists/history")
def get_playlist_history(
limit: int = Query(default=20, ge=1, le=100),
db: Session = Depends(get_db),
):
"""Returns historical playlist snapshots."""
snapshots = (
db.query(AnalysisSnapshot)
.filter(
(AnalysisSnapshot.playlist_theme.isnot(None))
| (AnalysisSnapshot.six_hour_playlist_id.isnot(None))
| (AnalysisSnapshot.daily_playlist_id.isnot(None))
)
.order_by(AnalysisSnapshot.date.desc())
.limit(limit)
.all()
)
result = []
for snap in snapshots:
result.append(
{
"id": snap.id,
"date": snap.date.isoformat() if snap.date else None,
"period_label": snap.period_label,
"theme": snap.playlist_theme,
"reasoning": snap.playlist_theme_reasoning,
"six_hour_id": snap.six_hour_playlist_id,
"daily_id": snap.daily_playlist_id,
"composition": snap.playlist_composition or [],
}
)
return {"history": result}