mirror of
https://github.com/bnair123/MusicAnalyser.git
synced 2026-02-25 11:46:07 +00:00
feat: implement AI-curated playlist service and dashboard integration
- Added hierarchical AGENTS.md knowledge base - Implemented PlaylistService with 6h themed and 24h devotion mix logic - Integrated AI theme generation for 6h playlists via Gemini/OpenAI - Added /playlists/refresh and metadata endpoints to API - Updated background worker with scheduled playlist curation - Created frontend PlaylistsSection, Tooltip components and integrated into Dashboard - Added Alembic migration for playlist tracking columns - Fixed Docker healthcheck with curl installation
This commit is contained in:
167
backend/app/services/playlist_service.py
Normal file
167
backend/app/services/playlist_service.py
Normal file
@@ -0,0 +1,167 @@
|
||||
import os
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .spotify_client import SpotifyClient
|
||||
from .reccobeats_client import ReccoBeatsClient
|
||||
from .narrative_service import NarrativeService
|
||||
|
||||
|
||||
class PlaylistService:
|
||||
def __init__(
|
||||
self,
|
||||
db: Session,
|
||||
spotify_client: SpotifyClient,
|
||||
recco_client: ReccoBeatsClient,
|
||||
narrative_service: NarrativeService,
|
||||
) -> None:
|
||||
self.db = db
|
||||
self.spotify = spotify_client
|
||||
self.recco = recco_client
|
||||
self.narrative = narrative_service
|
||||
|
||||
async def ensure_playlists_exist(self, user_id: str) -> Dict[str, str]:
|
||||
"""Check/create playlists. Returns {six_hour_id, daily_id}."""
|
||||
six_hour_env = os.getenv("SIX_HOUR_PLAYLIST_ID")
|
||||
daily_env = os.getenv("DAILY_PLAYLIST_ID")
|
||||
|
||||
if not six_hour_env:
|
||||
six_hour_data = await self.spotify.create_playlist(
|
||||
user_id=user_id,
|
||||
name="Short and Sweet",
|
||||
description="AI-curated 6-hour playlists based on your listening habits",
|
||||
)
|
||||
six_hour_env = str(six_hour_data["id"])
|
||||
|
||||
if not daily_env:
|
||||
daily_data = await self.spotify.create_playlist(
|
||||
user_id=user_id,
|
||||
name="Proof of Commitment",
|
||||
description="Your daily 24-hour mix showing your music journey",
|
||||
)
|
||||
daily_env = str(daily_data["id"])
|
||||
|
||||
return {"six_hour_id": str(six_hour_env), "daily_id": str(daily_env)}
|
||||
|
||||
async def curate_six_hour_playlist(
|
||||
self, period_start: datetime, period_end: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate 6-hour playlist (15 curated + 15 recommendations)."""
|
||||
from app.models import Track
|
||||
from app.services.stats_service import StatsService
|
||||
|
||||
stats = StatsService(self.db)
|
||||
data = stats.generate_full_report(period_start, period_end)
|
||||
|
||||
listening_data = {
|
||||
"peak_hour": data["time_habits"]["peak_hour"],
|
||||
"avg_energy": data["vibe"]["avg_energy"],
|
||||
"avg_valence": data["vibe"]["avg_valence"],
|
||||
"total_plays": data["volume"]["total_plays"],
|
||||
"top_artists": data["volume"]["top_artists"][:10],
|
||||
}
|
||||
|
||||
theme_result = self.narrative.generate_playlist_theme(listening_data)
|
||||
|
||||
curated_track_names = theme_result.get("curated_tracks", [])
|
||||
curated_tracks: List[str] = []
|
||||
for name in curated_track_names:
|
||||
track = self.db.query(Track).filter(Track.name.ilike(f"%{name}%")).first()
|
||||
if track:
|
||||
curated_tracks.append(str(track.id))
|
||||
|
||||
recommendations: List[str] = []
|
||||
if curated_tracks:
|
||||
recs = await self.recco.get_recommendations(
|
||||
seed_ids=curated_tracks[:5],
|
||||
size=15,
|
||||
)
|
||||
recommendations = [
|
||||
str(r.get("spotify_id") or r.get("id"))
|
||||
for r in recs
|
||||
if r.get("spotify_id") or r.get("id")
|
||||
]
|
||||
|
||||
final_tracks = curated_tracks[:15] + recommendations[:15]
|
||||
|
||||
playlist_id = os.getenv("SIX_HOUR_PLAYLIST_ID")
|
||||
if playlist_id:
|
||||
await self.spotify.update_playlist_details(
|
||||
playlist_id=playlist_id,
|
||||
name=f"Short and Sweet - {theme_result['theme_name']}",
|
||||
description=(
|
||||
f"{theme_result['description']}\n\nCurated: {len(curated_tracks)} tracks + {len(recommendations)} recommendations"
|
||||
),
|
||||
)
|
||||
await self.spotify.replace_playlist_tracks(
|
||||
playlist_id=playlist_id,
|
||||
track_uris=[f"spotify:track:{tid}" for tid in final_tracks],
|
||||
)
|
||||
|
||||
return {
|
||||
"playlist_id": playlist_id,
|
||||
"theme_name": theme_result["theme_name"],
|
||||
"description": theme_result["description"],
|
||||
"track_count": len(final_tracks),
|
||||
"curated_count": len(curated_tracks),
|
||||
"rec_count": len(recommendations),
|
||||
"refreshed_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
async def curate_daily_playlist(
|
||||
self, period_start: datetime, period_end: datetime
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate 24-hour playlist (30 favorites + 20 discoveries)."""
|
||||
from app.models import Track
|
||||
from app.services.stats_service import StatsService
|
||||
|
||||
stats = StatsService(self.db)
|
||||
data = stats.generate_full_report(period_start, period_end)
|
||||
|
||||
top_all_time = self._get_top_all_time_tracks(limit=30)
|
||||
recent_tracks = [track["id"] for track in data["volume"]["top_tracks"][:20]]
|
||||
|
||||
final_tracks = (top_all_time + recent_tracks)[:50]
|
||||
|
||||
playlist_id = os.getenv("DAILY_PLAYLIST_ID")
|
||||
theme_name = f"Proof of Commitment - {datetime.utcnow().date().isoformat()}"
|
||||
if playlist_id:
|
||||
await self.spotify.update_playlist_details(
|
||||
playlist_id=playlist_id,
|
||||
name=theme_name,
|
||||
description=(
|
||||
f"{theme_name} reflects the past 24 hours plus your all-time devotion."
|
||||
),
|
||||
)
|
||||
await self.spotify.replace_playlist_tracks(
|
||||
playlist_id=playlist_id,
|
||||
track_uris=[f"spotify:track:{tid}" for tid in final_tracks],
|
||||
)
|
||||
|
||||
return {
|
||||
"playlist_id": playlist_id,
|
||||
"theme_name": theme_name,
|
||||
"description": "Daily mix refreshed with your favorites and discoveries.",
|
||||
"track_count": len(final_tracks),
|
||||
"favorites_count": len(top_all_time),
|
||||
"recent_discoveries_count": len(recent_tracks),
|
||||
"refreshed_at": datetime.utcnow().isoformat(),
|
||||
}
|
||||
|
||||
def _get_top_all_time_tracks(self, limit: int = 30) -> List[str]:
|
||||
"""Get top tracks by play count from all-time history."""
|
||||
from app.models import PlayHistory, Track
|
||||
from sqlalchemy import func
|
||||
|
||||
result = (
|
||||
self.db.query(Track.id, func.count(PlayHistory.id).label("play_count"))
|
||||
.join(PlayHistory, Track.id == PlayHistory.track_id)
|
||||
.group_by(Track.id)
|
||||
.order_by(func.count(PlayHistory.id).desc())
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return [track_id for track_id, _ in result]
|
||||
Reference in New Issue
Block a user