| """ |
| 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" |
|
|
| |
| |
| |
| 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 [] |
| |
| 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 "<p>No tracks found — try again.</p>" |
|
|
| cards = "\n".join( |
| '<div style="display:flex;align-items:center;gap:12px;background:rgba(255,255,255,0.04);' |
| 'border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:10px;">' |
| f'<img src="{t["image"]}" style="width:56px;height:56px;border-radius:8px;object-fit:cover;flex-shrink:0;" />' |
| '<div style="flex:1;min-width:0;">' |
| f'<div style="font-weight:700;font-size:0.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' |
| f'<a href="{t["url"]}" target="_blank" style="color:#fff;text-decoration:none;">{t["name"]}</a></div>' |
| f'<div style="font-size:0.8rem;color:rgba(255,255,255,0.55);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{t["artists"]}</div>' |
| f'<audio controls preload="none" style="width:100%;height:32px;margin-top:6px;">' |
| f'<source src="{t["preview_url"]}" type="audio/mp4"></audio>' |
| '</div></div>' |
| for t in tracks |
| ) |
| return f'<div style="display:flex;flex-direction:column;gap:10px;">{cards}</div>' |
|
|