diff --git a/app/helper/lastfm_profile.py b/app/helper/lastfm_profile.py index 2e917b8..33bb34a 100644 --- a/app/helper/lastfm_profile.py +++ b/app/helper/lastfm_profile.py @@ -243,3 +243,119 @@ def get_artists_by_tag(tag, limit=10): pass return [] + + +def get_user_playlists(token, limit=50): + """ + Get all playlists of the current user from Spotify. + """ + url = "https://api.spotify.com/v1/me/playlists" + params = {"limit": limit} + headers = {"Authorization": f"Bearer {token}"} + + try: + response = requests.get(url, params=params, headers=headers, timeout=10) + if response.status_code == 200: + data = response.json() + return data.get("items", []) + except Exception: + pass + + return [] + + +def get_playlist_tracks(token, playlist_id, limit=50): + """ + Get tracks from a specific playlist. + """ + url = f"https://api.spotify.com/v1/playlists/{playlist_id}/tracks" + params = {"limit": limit, "fields": "items(track(name,artists,uri,album))"} + headers = {"Authorization": f"Bearer {token}"} + + try: + response = requests.get(url, params=params, headers=headers, timeout=10) + if response.status_code == 200: + data = response.json() + # Extract track objects from the response + tracks = [] + for item in data.get("items", []): + if item.get("track"): + tracks.append(item["track"]) + return tracks + except Exception: + pass + + return [] + + +def generate_playlist_from_profile_and_artist(profile, seed_artist, token, variety=5, discovery=5, limit=20): + """ + Generate a playlist combining: + - Profile mood/tags from analyzed playlist + - Seed artist for similar artists + - Variety (how many different artists) + - Discovery (balance between profile tags and seed artist) + """ + from app.helper.recommendations import get_similar_artists_lastfm, search_artist_top_tracks_spotify + + collected_tracks = [] + seen_uris = set() + + # Calculate how many tracks from profile tags vs seed artist + # Discovery: 1 = mostly profile, 10 = mostly seed artist + profile_ratio = 1 - (discovery / 10) + profile_tracks_target = max(1, int(limit * profile_ratio)) + seed_tracks_target = limit - profile_tracks_target + + # Part 1: Get tracks based on profile tags + if profile and profile.get('top_tags'): + tags_to_use = profile['top_tags'][:variety] + tracks_per_tag = max(1, profile_tracks_target // len(tags_to_use)) + 1 + + for tag, count in tags_to_use: + if len(collected_tracks) >= profile_tracks_target: + break + + tag_artists = get_artists_by_tag(tag, limit=3) + for artist in tag_artists: + if len(collected_tracks) >= profile_tracks_target: + break + + tracks = search_artist_top_tracks_spotify(artist, token, limit=2) + for track in tracks: + if track and track.get('uri') not in seen_uris: + collected_tracks.append(track) + seen_uris.add(track['uri']) + if len(collected_tracks) >= profile_tracks_target: + break + + # Part 2: Get tracks based on seed artist and similar artists + if seed_artist: + # Get seed artist's top tracks + seed_tracks = search_artist_top_tracks_spotify(seed_artist, token, limit=seed_tracks_target // 2 + 1) + for track in seed_tracks: + if track and track.get('uri') not in seen_uris: + collected_tracks.append(track) + seen_uris.add(track['uri']) + if len(collected_tracks) >= limit: + break + + # Get similar artists + similar_artists = get_similar_artists_lastfm(seed_artist, limit=variety) + for artist in similar_artists: + if len(collected_tracks) >= limit: + break + + tracks = search_artist_top_tracks_spotify(artist, token, limit=2) + for track in tracks: + if track and track.get('uri') not in seen_uris: + collected_tracks.append(track) + seen_uris.add(track['uri']) + if len(collected_tracks) >= limit: + break + + # Shuffle to mix profile and seed artist tracks + import random + random.shuffle(collected_tracks) + + return collected_tracks[:limit] diff --git a/app/main.py b/app/main.py index 742ed7c..aec3227 100755 --- a/app/main.py +++ b/app/main.py @@ -245,3 +245,134 @@ def generate_from_profile(): def playlist_analyzer(): """Redirect old analyzer route to new profile page.""" return redirect(url_for('music_profile')) + + +@app.route('/api/playlists', methods=['GET']) +def get_user_playlists(): + """ + Get all playlists of the current user. + """ + sess_access_token = session.get("access_token") + sess_token_create_time = session.get('token_create') + + if not sess_access_token or not sess_token_create_time: + return jsonify({"error": "Not authenticated"}), 403 + + if (time.time() - sess_token_create_time) > 3500: + return jsonify({"error": "Token expired"}), 400 + + playlists = lastfm_profile.get_user_playlists(sess_access_token, limit=50) + + # Return simplified playlist data + playlist_data = [] + for pl in playlists: + playlist_data.append({ + 'id': pl.get('id'), + 'name': pl.get('name'), + 'tracks_total': pl.get('tracks', {}).get('total', 0), + 'image': pl.get('images', [{}])[0].get('url') if pl.get('images') else None + }) + + return jsonify({"playlists": playlist_data}) + + +@app.route('/api/playlist//analyze', methods=['GET']) +def analyze_playlist(playlist_id): + """ + Analyze a specific playlist and return its mood profile. + """ + sess_access_token = session.get("access_token") + sess_token_create_time = session.get('token_create') + + if not sess_access_token or not sess_token_create_time: + return jsonify({"error": "Not authenticated"}), 403 + + if (time.time() - sess_token_create_time) > 3500: + return jsonify({"error": "Token expired"}), 400 + + # Get tracks from playlist + tracks = lastfm_profile.get_playlist_tracks(sess_access_token, playlist_id, limit=30) + + if not tracks: + return jsonify({"error": "Could not fetch playlist tracks"}), 400 + + # Analyze tracks + profile = lastfm_profile.analyze_tracks_profile(tracks) + + # Store in session for later use + session['playlist_profile'] = { + 'playlist_id': playlist_id, + 'scores': profile['scores'], + 'top_tags': profile['top_tags'][:10], + 'genres': profile['genres'] + } + + return jsonify({ + 'scores': profile['scores'], + 'top_tags': profile['top_tags'][:15], + 'genres': profile['genres'], + 'track_count': profile['track_count'] + }) + + +@app.route('/generate-from-playlist', methods=['POST']) +def generate_from_playlist(): + """ + Generate a new playlist based on: + - Mood profile from selected playlist + - Seed artist + - Variety and Discovery settings + """ + sess_access_token = session.get("access_token") + sess_token_create_time = session.get('token_create') + + if not sess_access_token or not sess_token_create_time: + return jsonify({"error": "Not authenticated"}), 403 + + if (time.time() - sess_token_create_time) > 3500: + return jsonify({"error": "Token expired"}), 400 + + data = request.get_json() + playlist_id = data.get('playlist_id') + seed_artist = data.get('seed_artist', '').strip() + variety = int(data.get('variety', 5)) + discovery = int(data.get('discovery', 5)) + track_count = int(data.get('track_count', 15)) + + # Get or create profile from playlist + playlist_profile = session.get('playlist_profile') + + # If no profile or different playlist, analyze it + if not playlist_profile or playlist_profile.get('playlist_id') != playlist_id: + if playlist_id: + tracks = lastfm_profile.get_playlist_tracks(sess_access_token, playlist_id, limit=30) + if tracks: + profile = lastfm_profile.analyze_tracks_profile(tracks) + playlist_profile = { + 'playlist_id': playlist_id, + 'scores': profile['scores'], + 'top_tags': profile['top_tags'][:10], + 'genres': profile['genres'] + } + session['playlist_profile'] = playlist_profile + + if not playlist_profile: + return jsonify({"error": "No playlist profile available"}), 400 + + # Generate tracks + tracks = lastfm_profile.generate_playlist_from_profile_and_artist( + playlist_profile, + seed_artist, + sess_access_token, + variety=variety, + discovery=discovery, + limit=track_count + ) + + uris = [t['uri'] for t in tracks if t] + return jsonify({ + "songs": tracks, + "uris": json.dumps(uris), + "profile_tags": playlist_profile['top_tags'][:5], + "seed_artist": seed_artist + }) diff --git a/app/templates/index.html b/app/templates/index.html index 29a1475..7c92987 100755 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,152 +1,365 @@ - Playlist Generator - + - + - - -
-

Magic Music Generator

by marcify -
- -
- -
- -
-

Playlist Settings

-
-
- -
- - -
- -
-
- -
- - -
+ -
- - -
+
+

Magic Music Generator

+

Create playlists based on the mood of your existing playlists

+
-
+
+ +
+

1 Select a Playlist to Analyze

+

Choose one of your playlists as the mood template

-
- - -
+
+

Loading your playlists...

+
-
-
+
+ +
- -
- - -
+ +
-
-
-
+ +
+

2 Playlist Mood Profile

+

Based on

- +
+
+
Mood Scores
+
+
+
+
Top Tags
+
+
+
- -
+
- +
+ + + 1 = mostly mood profile, 10 = mostly seed artist +
-
-

Recommendations

-
-
+
+ + +
+
- -
-
+
+
Generated Playlist
+
+

Click "Generate Playlist" to see recommendations

+
-
+ +
+
+
+
- -