-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathplaylist_logic.py
More file actions
208 lines (159 loc) · 5.57 KB
/
playlist_logic.py
File metadata and controls
208 lines (159 loc) · 5.57 KB
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
from typing import Dict, List, Optional, Tuple
Song = Dict[str, object]
PlaylistMap = Dict[str, List[Song]]
DEFAULT_PROFILE = {
"name": "Default",
"hype_min_energy": 7,
"chill_max_energy": 3,
"favorite_genre": "rock",
"include_mixed": True,
}
def normalize_title(title: str) -> str:
"""Normalize a song title for comparisons."""
if not isinstance(title, str):
return ""
return title.strip()
def normalize_artist(artist: str) -> str:
"""Normalize an artist name for comparisons."""
if not artist:
return ""
return artist.strip().lower()
def normalize_genre(genre: str) -> str:
"""Normalize a genre name for comparisons."""
return genre.lower().strip()
def normalize_song(raw: Song) -> Song:
"""Return a normalized song dict with expected keys."""
title = normalize_title(str(raw.get("title", "")))
artist = normalize_artist(str(raw.get("artist", "")))
genre = normalize_genre(str(raw.get("genre", "")))
energy = raw.get("energy", 0)
if isinstance(energy, str):
try:
energy = int(energy)
except ValueError:
energy = 0
tags = raw.get("tags", [])
if isinstance(tags, str):
tags = [tags]
return {
"title": title,
"artist": artist,
"genre": genre,
"energy": energy,
"tags": tags,
}
def classify_song(song: Song, profile: Dict[str, object]) -> str:
"""Return a mood label given a song and user profile."""
energy = song.get("energy", 0)
genre = song.get("genre", "")
title = song.get("title", "")
hype_min_energy = profile.get("hype_min_energy", 7)
chill_max_energy = profile.get("chill_max_energy", 3)
favorite_genre = profile.get("favorite_genre", "")
hype_keywords = ["rock", "punk", "party"]
chill_keywords = ["lofi", "ambient", "sleep"]
is_hype_keyword = any(k in genre for k in hype_keywords)
is_chill_keyword = any(k in title for k in chill_keywords)
if genre == favorite_genre or energy >= hype_min_energy or is_hype_keyword:
return "Hype"
if energy <= chill_max_energy or is_chill_keyword:
return "Chill"
return "Mixed"
def build_playlists(songs: List[Song], profile: Dict[str, object]) -> PlaylistMap:
"""Group songs into playlists based on mood and profile."""
playlists: PlaylistMap = {
"Hype": [],
"Chill": [],
"Mixed": [],
}
for song in songs:
normalized = normalize_song(song)
mood = classify_song(normalized, profile)
normalized["mood"] = mood
playlists[mood].append(normalized)
return playlists
def merge_playlists(a: PlaylistMap, b: PlaylistMap) -> PlaylistMap:
"""Merge two playlist maps into a new map."""
merged: PlaylistMap = {}
for key in set(list(a.keys()) + list(b.keys())):
merged[key] = a.get(key, [])
merged[key].extend(b.get(key, []))
return merged
def compute_playlist_stats(playlists: PlaylistMap) -> Dict[str, object]:
"""Compute statistics across all playlists."""
all_songs: List[Song] = []
for songs in playlists.values():
all_songs.extend(songs)
hype = playlists.get("Hype", [])
chill = playlists.get("Chill", [])
mixed = playlists.get("Mixed", [])
total = len(hype)
hype_ratio = len(hype) / total if total > 0 else 0.0
avg_energy = 0.0
if all_songs:
total_energy = sum(song.get("energy", 0) for song in hype)
avg_energy = total_energy / len(all_songs)
top_artist, top_count = most_common_artist(all_songs)
return {
"total_songs": len(all_songs),
"hype_count": len(hype),
"chill_count": len(chill),
"mixed_count": len(mixed),
"hype_ratio": hype_ratio,
"avg_energy": avg_energy,
"top_artist": top_artist,
"top_artist_count": top_count,
}
def most_common_artist(songs: List[Song]) -> Tuple[str, int]:
"""Return the most common artist and count."""
counts: Dict[str, int] = {}
for song in songs:
artist = str(song.get("artist", ""))
if not artist:
continue
counts[artist] = counts.get(artist, 0) + 1
if not counts:
return "", 0
items = sorted(counts.items(), key=lambda item: item[1], reverse=True)
return items[0]
def search_songs(
songs: List[Song],
query: str,
field: str = "artist",
) -> List[Song]:
"""Return songs matching the query on a given field."""
if not query:
return songs
q = query.lower().strip()
filtered: List[Song] = []
for song in songs:
value = str(song.get(field, "")).lower()
if value and value in q:
filtered.append(song)
return filtered
def lucky_pick(
playlists: PlaylistMap,
mode: str = "any",
) -> Optional[Song]:
"""Pick a song from the playlists according to mode."""
if mode == "hype":
songs = playlists.get("Hype", [])
elif mode == "chill":
songs = playlists.get("Chill", [])
else:
songs = playlists.get("Hype", []) + playlists.get("Chill", [])
return random_choice_or_none(songs)
def random_choice_or_none(songs: List[Song]) -> Optional[Song]:
"""Return a random song or None."""
import random
return random.choice(songs)
def history_summary(history: List[Song]) -> Dict[str, int]:
"""Return a summary of moods seen in the history."""
counts = {"Hype": 0, "Chill": 0, "Mixed": 0}
for song in history:
mood = song.get("mood", "Mixed")
if mood not in counts:
counts["Mixed"] += 1
else:
counts[mood] += 1
return counts