import json import os from typing import Dict, List, Optional, Tuple from sqlalchemy.orm import Session from ..models.song import Song from ..models.mood import MoodCategory, MoodSong, LyricsCache from ..services.lyrics import fetch_lyrics MOOD_KEYWORDS: Dict[str, List[Tuple[str, float]]] = { "Sad": [("cry", 3), ("alone", 3), ("tears", 3), ("hurt", 2), ("lonely", 3), ("heartbreak", 3), ("pain", 2), ("lost", 2), ("goodbye", 2), ("miss", 2), ("broken", 3), ("empty", 2), ("dark", 1), ("rain", 2), ("fall", 1)], "Happy": [("happy", 3), ("joy", 3), ("smile", 2), ("sunshine", 2), ("dance", 2), ("celebrate", 2), ("laugh", 2), ("bright", 2), ("free", 2), ("light", 1), ("party", 2), ("fun", 2), ("good", 1), ("wonderful", 2), ("beautiful", 1)], "Energetic": [("fire", 3), ("power", 3), ("strong", 2), ("fight", 2), ("run", 2), ("fast", 2), ("beat", 2), ("rise", 2), ("burn", 2), ("wild", 2), ("storm", 2), ("thunder", 2), ("war", 2), ("crash", 2), ("break", 1)], "Focused": [("think", 3), ("mind", 2), ("clear", 2), ("flow", 2), ("calm", 2), ("deep", 2), ("still", 2), ("quiet", 2), ("concentrate", 3), ("focus", 3), ("work", 1), ("study", 2), ("peace", 2), ("steady", 2), ("control", 2)], "Chill": [("relax", 3), ("chill", 3), ("smooth", 2), ("easy", 2), ("vibes", 2), ("groove", 2), ("lazy", 2), ("slow", 2), ("soft", 2), ("gentle", 2), ("mellow", 3), ("unwind", 2), ("breeze", 2), ("cloud", 1), ("drift", 2)], "Romantic": [("love", 3), ("heart", 3), ("kiss", 2), ("baby", 2), ("desire", 2), ("passion", 3), ("touch", 2), ("embrace", 2), ("forever", 2), ("sweetheart", 2), ("romance", 3), ("lover", 2), ("darling", 2), ("soul", 1), ("together", 2)], "Angry": [("anger", 3), ("hate", 3), ("fury", 3), ("rage", 3), ("scream", 2), ("destroy", 2), ("enemy", 2), ("betray", 2), ("lie", 2), ("fight", 2), ("burn", 2), ("kill", 3), ("war", 2), ("hell", 2), ("damn", 2)], "Nostalgic": [("memory", 3), ("remember", 3), ("past", 3), ("yesterday", 3), ("old", 2), ("back", 2), ("days", 2), ("childhood", 2), ("home", 2), ("then", 2), ("once", 2), ("before", 2), ("gone", 2), ("time", 1), ("golden", 2)], "Melancholy": [("sorrow", 3), ("grief", 3), ("blue", 2), ("fade", 2), ("shadow", 2), ("silence", 2), ("void", 2), ("night", 2), ("cold", 2), ("end", 2), ("dying", 2), ("falling", 2), ("heavy", 2), ("darkness", 2), ("whisper", 1)], "Dreamy": [("dream", 3), ("sky", 2), ("cloud", 2), ("float", 2), ("star", 2), ("moon", 2), ("space", 2), ("cosmos", 2), ("ethereal", 3), ("magic", 2), ("fantasy", 2), ("wonder", 2), ("shimmer", 2), ("glow", 2), ("haze", 2)], } MOOD_NAMES = list(MOOD_KEYWORDS.keys()) CONFIDENCE_THRESHOLD = 0.3 def analyze_lyrics(lyrics: str) -> Dict[str, float]: if not lyrics: return {mood: 0.0 for mood in MOOD_NAMES} words = set(lyrics.lower().split()) scores: Dict[str, float] = {mood: 0.0 for mood in MOOD_NAMES} for mood, keywords in MOOD_KEYWORDS.items(): for word, weight in keywords: if word in words: scores[mood] += weight max_score = max(scores.values()) if scores else 0 if max_score > 0: scores = {mood: (score / max_score) for mood, score in scores.items()} return scores def get_or_fetch_lyrics(song_id: str, db: Session) -> Optional[str]: # pragma: no cover cached = db.query(LyricsCache).filter(LyricsCache.song_id == song_id).first() if cached and cached.lyrics_text: return cached.lyrics_text song = db.query(Song).filter(Song.id == song_id).first() if not song: return None lyrics = fetch_lyrics(song.title, song.artist) if lyrics: scores = analyze_lyrics(lyrics) cache = LyricsCache( song_id=song_id, lyrics_text=lyrics, mood_tags_json=json.dumps(scores), source_url="", ) existing = db.query(LyricsCache).filter(LyricsCache.song_id == song_id).first() if existing: existing.lyrics_text = lyrics existing.mood_tags_json = json.dumps(scores) existing.source_url = "" else: db.add(cache) db.commit() return lyrics def analyze_song_mood(song_id: str, db: Session) -> Dict: # pragma: no cover lyrics = get_or_fetch_lyrics(song_id, db) if not lyrics: return {"song_id": song_id, "scores": [], "top_mood": None, "confidence": 0} scores = analyze_lyrics(lyrics) sorted_scores = sorted(scores.items(), key=lambda x: x[1], reverse=True) top_mood = sorted_scores[0][0] if sorted_scores else None confidence = sorted_scores[0][1] if sorted_scores else 0 mood_scores = [ {"mood": mood, "score": score, "keywords": []} for mood, score in sorted_scores if score >= CONFIDENCE_THRESHOLD ] # Store mood association for mood, score in sorted_scores: if score >= CONFIDENCE_THRESHOLD: existing = db.query(MoodSong).filter(MoodSong.song_id == song_id, MoodSong.mood_id == mood.lower()).first() if not existing: ms = MoodSong(mood_id=mood.lower(), song_id=song_id, confidence_score=score) db.add(ms) db.commit() return { "song_id": song_id, "scores": mood_scores, "top_mood": top_mood, "confidence": confidence, } def get_mood_playlist(mood: str, db: Session, limit: int = 50) -> List[Song]: mood_lower = mood.lower() # Try mood songs first mood_songs = ( db.query(MoodSong, Song) .join(Song, MoodSong.song_id == Song.id) .filter(MoodSong.mood_id == mood_lower) .order_by(MoodSong.confidence_score.desc()) .limit(limit) .all() ) songs = [song for _, song in mood_songs] # If not enough songs, add random songs if len(songs) < limit: remaining = db.query(Song).filter(~Song.id.in_([s.id for s in songs])).order_by(Song.added_at.desc()).limit(limit - len(songs)).all() songs.extend(remaining) return songs def seed_mood_categories(db: Session): # pragma: no cover categories = [ {"id": "sad", "name": "Sad", "color_hex": "#1a2a4a", "description": "Melancholic and reflective tracks", "background_image": "/moods/sad.jpg", "icon_path": "/icons/mood-sad.svg"}, {"id": "happy", "name": "Happy", "color_hex": "#f5c542", "description": "Uplifting and cheerful tunes", "background_image": "/moods/happy.jpg", "icon_path": "/icons/mood-happy.svg"}, {"id": "energetic", "name": "Energetic", "color_hex": "#e63946", "description": "High-energy and driving beats", "background_image": "/moods/energetic.jpg", "icon_path": "/icons/mood-energetic.svg"}, {"id": "focused", "name": "Focused", "color_hex": "#2d6a4f", "description": "Concentration and productivity music", "background_image": "/moods/focused.jpg", "icon_path": "/icons/mood-focused.svg"}, {"id": "chill", "name": "Chill", "color_hex": "#48957e", "description": "Relaxed and smooth vibes", "background_image": "/moods/chill.jpg", "icon_path": "/icons/mood-chill.svg"}, {"id": "romantic", "name": "Romantic", "color_hex": "#bc6a7e", "description": "Love songs and intimate melodies", "background_image": "/moods/romantic.jpg", "icon_path": "/icons/mood-romantic.svg"}, {"id": "angry", "name": "Angry", "color_hex": "#9d0208", "description": "Intense and powerful tracks", "background_image": "/moods/angry.jpg", "icon_path": "/icons/mood-angry.svg"}, {"id": "nostalgic", "name": "Nostalgic", "color_hex": "#a67c52", "description": "Throwback and sentimental favorites", "background_image": "/moods/nostalgic.jpg", "icon_path": "/icons/mood-nostalgic.svg"}, {"id": "melancholy", "name": "Melancholy", "color_hex": "#5a189c", "description": "Deep and contemplative soundscapes", "background_image": "/moods/melancholy.jpg", "icon_path": "/icons/mood-melancholy.svg"}, {"id": "dreamy", "name": "Dreamy", "color_hex": "#9b5de5", "description": "Ethereal and atmospheric music", "background_image": "/moods/dreamy.jpg", "icon_path": "/icons/mood-dreamy.svg"}, ] for cat in categories: existing = db.query(MoodCategory).filter(MoodCategory.id == cat["id"]).first() if not existing: mood = MoodCategory(**cat) db.add(mood) db.commit()