""" Music integration for Moodwave (Apple iTunes Search API). Uses Apple's free, keyless iTunes Search API (https://itunes.apple.com/search) to find tracks that match a detected mood. No account, API key, or paid subscription of any kind is required. NOTE: This module previously used the Spotify Web API (Client Credentials flow). As of Feb 2026 Spotify requires a Spotify Premium account for any Web API "Development Mode" access (see Spotify's Feb 2026 developer changelog), so this module was switched to the iTunes Search API instead, which has no such restriction. """ import random import requests SEARCH_URL = "https://itunes.apple.com/search" # Hand-picked free-text search phrases per mood. Plain free-text search # performs better against the iTunes track index than trying to filter # by genre. MOOD_QUERIES = { "joy": [ "happy upbeat pop", "feel good dance hits", "uplifting summer anthems", "energetic feel-good songs", ], "sadness": [ "sad acoustic ballads", "melancholy piano songs", "heartbreak slow songs", "rainy day sad songs", ], "anger": [ "aggressive rock anthems", "angry metal songs", "rage rap", "intense hard rock", ], "fear": [ "dark ambient tense", "eerie atmospheric soundtrack", "anxious moody electronic", "suspenseful score", ], "neutral": [ "chill lofi beats", "calm instrumental", "relaxing background music", "easy listening chill", ], } MOOD_EMOJI = { "joy": "🟢", "sadness": "🔵", "anger": "🔴", "fear": "🟣", "neutral": "⚪", } def spotify_configured() -> bool: """Kept for backward compatibility with app.py's import - the iTunes Search API needs no credentials, so this is always available.""" return True def search_tracks_for_mood(mood: str, n: int = 5): """ Return up to `n` tracks matching the given mood via the iTunes Search API. Each track is a dict: {name, artists, url, preview_url, image}. Raises RuntimeError with a human-readable message on failure. """ mood = (mood or "neutral").lower() if mood not in MOOD_QUERIES: mood = "neutral" query = random.choice(MOOD_QUERIES[mood]) try: resp = requests.get( SEARCH_URL, params={ "term": query, "media": "music", "entity": "song", "limit": 25, "country": "US", }, timeout=10, ) resp.raise_for_status() except requests.RequestException as e: raise RuntimeError(f"Couldn't reach the music search service ({e}). Try again.") items = resp.json().get("results", []) or [] # Only keep results that actually have a playable 30-second preview. items = [t for t in items if t.get("previewUrl")] if not items: return [] random.shuffle(items) chosen = items[:n] tracks = [] for t in chosen: artwork = t.get("artworkUrl100", "") tracks.append( { "name": t.get("trackName", "Unknown"), "artists": t.get("artistName", ""), "url": t.get("trackViewUrl", "#"), "preview_url": t.get("previewUrl", ""), "image": artwork.replace("100x100", "300x300") if artwork else "", } ) return tracks def tracks_to_html(tracks, mood: str) -> str: """Render a list of track dicts as a column of preview-player cards.""" if not tracks: return "
No tracks found — try again.
" cards = "\n".join( '