File size: 4,746 Bytes
8faa100
b6d847f
8faa100
b6d847f
 
 
8faa100
b6d847f
 
 
 
 
8faa100
 
 
 
 
 
b6d847f
8faa100
 
b6d847f
 
8faa100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b6d847f
 
 
8faa100
 
 
 
b6d847f
 
 
8faa100
 
 
 
 
b6d847f
 
 
 
 
 
 
 
 
 
 
 
8faa100
b6d847f
 
 
8faa100
b6d847f
 
 
8faa100
 
 
 
 
 
 
 
b6d847f
8faa100
 
b6d847f
 
 
 
 
8faa100
 
 
 
 
 
b6d847f
8faa100
 
 
 
b6d847f
 
 
 
 
 
 
 
 
 
8faa100
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""
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 "<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>'