mirror of
https://github.com/bnair123/MusicAnalyser.git
synced 2026-02-25 19:56:06 +00:00
- 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
168 lines
6.3 KiB
Python
168 lines
6.3 KiB
Python
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]
|