import streamlit as st import spotipy from spotipy.oauth2 import SpotifyOAuth import requests import random import time from PIL import Image import io import re import json import urllib.parse from textblob import TextBlob from collections import Counter import numpy as np # Page config st.set_page_config( page_title="MoodTunes AI", page_icon="🎵", layout="wide", initial_sidebar_state="expanded" ) # Custom CSS st.markdown(""" """, unsafe_allow_html=True) # Spotify configuration SPOTIFY_CLIENT_ID = "ebf3b2530aed4b3b916f95c6e8fed6b6" SPOTIFY_CLIENT_SECRET = st.secrets.get("SPOTIFY_CLIENT_SECRET", "your_client_secret_here") REDIRECT_URI = "https://kilgorepennington-mood2music.hf.space/" # Initialize session state if 'spotify_token' not in st.session_state: st.session_state.spotify_token = None if 'user_info' not in st.session_state: st.session_state.user_info = None if 'generated_playlist' not in st.session_state: st.session_state.generated_playlist = None if 'auth_step' not in st.session_state: st.session_state.auth_step = 'start' if 'current_track_index' not in st.session_state: st.session_state.current_track_index = 0 if 'is_playing' not in st.session_state: st.session_state.is_playing = False def get_spotify_oauth(): """Create Spotify OAuth object""" return SpotifyOAuth( client_id=SPOTIFY_CLIENT_ID, client_secret=SPOTIFY_CLIENT_SECRET, redirect_uri=REDIRECT_URI, scope="user-read-private user-read-email playlist-modify-public playlist-modify-private user-read-currently-playing user-read-playback-state user-modify-playback-state streaming" ) def get_spotify_auth_url(): """Get Spotify authorization URL""" sp_oauth = get_spotify_oauth() return sp_oauth.get_authorize_url() def exchange_code_for_token(auth_code): """Exchange authorization code for access token""" try: sp_oauth = get_spotify_oauth() token_info = sp_oauth.get_access_token(auth_code, as_dict=True) if token_info and 'access_token' in token_info: return token_info['access_token'] return None except Exception as e: st.error(f"Token exchange failed: {e}") return None def get_spotify_client(token): """Get authenticated Spotify client""" return spotipy.Spotify(auth=token) def fetch_song_lyrics(artist, song_title): """Fetch lyrics from multiple free APIs""" clean_artist = re.sub(r'\s*\(feat\..*?\)', '', artist).strip() clean_song = re.sub(r'\s*\(feat\..*?\)', '', song_title).strip() apis = [ f"https://api.lyrics.ovh/v1/{clean_artist}/{clean_song}", ] for api_url in apis: try: response = requests.get(api_url, timeout=3) if response.status_code == 200: data = response.json() lyrics = data.get('lyrics', '').strip() if lyrics and len(lyrics) > 50: return lyrics except: continue return None def analyze_emotional_themes(text): """Analyze emotional themes in text using semantic analysis""" if not text: return {} text_lower = text.lower() emotional_themes = { 'anger': { 'words': ['angry', 'mad', 'rage', 'furious', 'hate', 'pissed', 'livid', 'irritated', 'annoyed', 'frustrated', 'outraged', 'heated', 'steaming', 'boiling'], 'phrases': ['want to fight', 'so angry', 'makes me mad', 'hate this', 'fed up', 'bar fight'], 'concepts': ['destroy', 'break', 'smash', 'crush', 'explode', 'violence', 'attack', 'fight'] }, 'drinking': { 'words': ['whiskey', 'bourbon', 'beer', 'drunk', 'drinking', 'alcohol', 'bar', 'tavern', 'bottle', 'glass', 'shot', 'liquor', 'vodka', 'rum', 'wine'], 'phrases': ['drinking mood', 'get drunk', 'hit the bar', 'drown sorrows'], 'concepts': ['party', 'celebration', 'drowning sorrows', 'liquid courage'] }, 'wild': { 'words': ['wild', 'crazy', 'howl', 'moon', 'primal', 'beast', 'savage', 'untamed'], 'phrases': ['howl at moon', 'go wild', 'lose control'], 'concepts': ['freedom', 'unleashed', 'primitive', 'instinct'] }, 'rebellion': { 'words': ['rebel', 'fight', 'resist', 'defy', 'revolt', 'against', 'system', 'authority', 'control', 'freedom', 'break free'], 'phrases': ['fight the system', 'break the rules', 'stand up'], 'concepts': ['liberation', 'independence', 'defiance', 'uprising'] }, 'sadness': { 'words': ['sad', 'depressed', 'lonely', 'empty', 'broken', 'hurt', 'pain', 'cry', 'tears', 'sorrow', 'grief', 'misery'], 'phrases': ['so sad', 'feel empty', 'broken heart'], 'concepts': ['loss', 'goodbye', 'missing', 'alone', 'darkness'] }, 'empowerment': { 'words': ['strong', 'powerful', 'confident', 'fierce', 'bold', 'brave', 'warrior', 'champion', 'victory', 'winner', 'succeed'], 'phrases': ['feel strong', 'take control', 'stand tall'], 'concepts': ['strength', 'power', 'control', 'dominance', 'success'] } } theme_scores = {} for theme, theme_data in emotional_themes.items(): score = 0 for word in theme_data['words']: if word in text_lower: score += 2 for phrase in theme_data['phrases']: if phrase in text_lower: score += 3 for concept in theme_data['concepts']: if concept in text_lower: score += 1 theme_scores[theme] = score try: blob = TextBlob(text[:500]) sentiment = blob.sentiment.polarity if sentiment < -0.3: for theme in ['anger', 'sadness']: if theme in theme_scores: theme_scores[theme] += 1 except: sentiment = 0 return theme_scores def calculate_theme_similarity(user_themes, lyric_themes): """Calculate similarity between user themes and song lyric themes""" if not user_themes or not lyric_themes: return 0 similarity = 0 for theme, user_score in user_themes.items(): if user_score > 0 and theme in lyric_themes: lyric_score = lyric_themes[theme] if lyric_score > 0: similarity += min(user_score, 3) * min(lyric_score, 3) matching_themes = sum(1 for theme in user_themes.keys() if user_themes.get(theme, 0) > 0 and lyric_themes.get(theme, 0) > 0) if matching_themes > 1: similarity += matching_themes * 0.5 return similarity def create_playlist_name_from_description(description): """Create a creative playlist name based on user description""" desc_lower = description.lower() # Extract key phrases and emotions if 'whiskey' in desc_lower and 'bar fight' in desc_lower: return "Whiskey Rage & Bar Fights" elif 'whiskey' in desc_lower and 'moon' in desc_lower: return "Whiskey Under the Moon" elif 'drunk' in desc_lower and 'rage' in desc_lower: return "Drunken Fury" elif 'broken heart' in desc_lower: return "Broken Heart Ballads" elif 'workout' in desc_lower or 'gym' in desc_lower: return "Pump Up Power" elif 'nostalgic' in desc_lower: return "Nostalgic Memories" elif 'celebration' in desc_lower or 'party' in desc_lower: return "Celebration Vibes" elif 'angry' in desc_lower or 'pissed' in desc_lower: return "Rage Mode" elif 'sad' in desc_lower or 'down' in desc_lower: return "Melancholy Moods" elif 'love' in desc_lower and 'lost' in desc_lower: return "Lost Love Chronicles" else: # Extract first few meaningful words words = re.findall(r'\b[a-z]{4,}\b', desc_lower) meaningful_words = [w for w in words[:3] if w not in ['feeling', 'really', 'want', 'need', 'like']] if meaningful_words: return f"{' '.join(meaningful_words[:2]).title()} Vibes" else: return "Custom Mood Mix" def search_filtered_songs(spotify_client, user_description, selected_genres, selected_decade): """Search for songs with genre and decade filters""" all_candidates = [] # Analyze themes first user_themes = analyze_emotional_themes(user_description) # Create base thematic searches thematic_searches = [] if user_themes.get('anger', 0) > 0: thematic_searches.extend(['angry songs', 'aggressive music', 'rage']) if user_themes.get('drinking', 0) > 0: thematic_searches.extend(['drinking songs', 'whiskey', 'bar songs']) if user_themes.get('wild', 0) > 0: thematic_searches.extend(['wild music', 'howling', 'primal']) if user_themes.get('rebellion', 0) > 0: thematic_searches.extend(['rebellion songs', 'fight songs']) if user_themes.get('sadness', 0) > 0: thematic_searches.extend(['sad songs', 'heartbreak']) if user_themes.get('empowerment', 0) > 0: thematic_searches.extend(['empowerment songs', 'strong music']) # If no specific themes, use general searches if not thematic_searches: thematic_searches = ['emotional songs', 'intense music'] # Add genre filters to searches if 'All Genres' not in selected_genres: genre_searches = [] for genre in selected_genres: for theme in thematic_searches[:3]: # Limit for performance genre_searches.append(f'genre:"{genre}" {theme}') # Also search pure genre genre_searches.append(f'genre:"{genre}"') search_terms = genre_searches + thematic_searches else: search_terms = thematic_searches # Execute searches with decade filter for search_term in search_terms[:8]: # Limit searches try: # Add year filter if specified if selected_decade != "Any Year": start_year = int(selected_decade[:4]) end_year = start_year + 9 search_query = f'{search_term} year:{start_year}-{end_year}' else: search_query = search_term results = spotify_client.search(q=search_query, type='track', limit=20, market='US') for track in results['tracks']['items']: # Additional decade filtering (Spotify's year filter isn't always perfect) track_year = None if track['album']['release_date']: try: track_year = int(track['album']['release_date'][:4]) except: pass # Check decade filter if selected_decade != "Any Year": decade_start = int(selected_decade[:4]) decade_end = decade_start + 9 if track_year and (track_year < decade_start or track_year > decade_end): continue all_candidates.append({ 'name': track['name'], 'artist': track['artists'][0]['name'], 'id': track['id'], 'popularity': track['popularity'], 'year': track_year }) except Exception as e: continue # Remove duplicates seen = set() unique_candidates = [] for song in all_candidates: key = (song['name'], song['artist']) if key not in seen: seen.add(key) unique_candidates.append(song) return unique_candidates[:80] def create_advanced_playlist(spotify_client, user_description, selected_genres, selected_decade, user_id): """Create playlist with advanced filtering and thematic analysis""" # Analyze user's emotional themes user_themes = analyze_emotional_themes(user_description) # Create playlist name playlist_name = create_playlist_name_from_description(user_description) st.info(f"🎭 Detected themes: {', '.join([k for k, v in user_themes.items() if v > 0])}") st.info(f"🎸 Genres: {', '.join(selected_genres)}") st.info(f"📅 Era: {selected_decade}") # Get filtered candidates candidate_songs = search_filtered_songs(spotify_client, user_description, selected_genres, selected_decade) st.info(f"🔍 Analyzing {len(candidate_songs)} filtered songs...") if not candidate_songs: return None matched_songs = [] progress_bar = st.progress(0) analyzed_count = 0 # Analyze lyrics for thematic similarity for i, song in enumerate(candidate_songs): if analyzed_count >= 50: # Limit for performance break progress_bar.progress((analyzed_count + 1) / 50) lyrics = fetch_song_lyrics(song['artist'], song['name']) if lyrics: analyzed_count += 1 lyric_themes = analyze_emotional_themes(lyrics) similarity_score = calculate_theme_similarity(user_themes, lyric_themes) if similarity_score > 1: matched_songs.append({ 'name': song['name'], 'artist': song['artist'], 'id': song['id'], 'similarity_score': similarity_score, 'popularity': song.get('popularity', 0), 'year': song.get('year'), 'matching_themes': [k for k in user_themes.keys() if user_themes.get(k, 0) > 0 and lyric_themes.get(k, 0) > 0] }) else: # Include some songs without lyrics based on popularity and filters if song.get('popularity', 0) > 40: matched_songs.append({ 'name': song['name'], 'artist': song['artist'], 'id': song['id'], 'similarity_score': 0.5, # Lower score for no lyrics 'popularity': song.get('popularity', 0), 'year': song.get('year'), 'matching_themes': [] }) time.sleep(0.05) progress_bar.empty() # Sort by similarity score and select top matches matched_songs.sort(key=lambda x: (x['similarity_score'], x['popularity']/100), reverse=True) final_songs = matched_songs[:20] if final_songs: st.success(f"✅ Found {len(final_songs)} matching songs!") if final_songs[0]['matching_themes']: st.info(f"🎯 Top songs share themes: {', '.join(final_songs[0]['matching_themes'][:3])}") return create_playlist_from_matched_songs(spotify_client, final_songs, user_id, user_description, playlist_name) else: st.warning("❌ No songs found matching your criteria.") return None def create_playlist_from_matched_songs(spotify_client, matched_songs, user_id, description, playlist_name): """Create the final playlist from matched songs""" try: playlist = spotify_client.user_playlist_create( user=user_id, name=playlist_name, public=False, description=f"AI-curated playlist: {description[:100]}..." ) track_ids = [song['id'] for song in matched_songs] spotify_client.playlist_add_items(playlist['id'], track_ids) return { 'name': playlist_name, 'tracks': [{'name': s['name'], 'artist': s['artist']} for s in matched_songs], 'track_count': len(matched_songs), 'playlist_url': playlist['external_urls']['spotify'], 'playlist_id': playlist['id'], 'is_demo': False, 'method': 'advanced_thematic', 'mood': 'Custom', 'energy': 'Variable' } except Exception as e: st.error(f"Failed to create playlist: {e}") return None def analyze_mood(description): """Basic mood analysis""" return { 'primary_emotion': 'custom', 'energy_level': 'variable', 'description': description } def create_demo_playlist(mood_analysis): """Create demo playlist""" tracks = [ {'name': 'Whiskey River', 'artist': 'Willie Nelson'}, {'name': 'Friends in Low Places', 'artist': 'Garth Brooks'}, {'name': 'Copperhead Road', 'artist': 'Steve Earle'}, {'name': 'Bad to the Bone', 'artist': 'George Thorogood'} ] return { 'name': "Demo: Whiskey & Rebellion", 'mood': 'Demo', 'energy': 'Variable', 'tracks': tracks, 'track_count': len(tracks), 'is_demo': True } @st.cache_data(show_spinner=False) def generate_image_pollinations(prompt, width=300, height=300): """Generate image using Pollinations AI""" try: clean_prompt = prompt.replace(" ", "%20").replace(",", "%2C") url = f"https://image.pollinations.ai/prompt/{clean_prompt}?width={width}&height={height}&seed={random.randint(1, 10000)}" response = requests.get(url, timeout=20) if response.status_code == 200: image = Image.open(io.BytesIO(response.content)) return image return None except Exception as e: return None def generate_demo_artwork(track): """Generate demo artwork""" prompt = f"album cover for {track['name']} by {track['artist']}, music artwork, artistic" return generate_image_pollinations(prompt, 300, 300) # Main App def main(): st.markdown('
Create playlists with advanced filtering and thematic lyrical analysis!
', unsafe_allow_html=True) # Authentication code (same as before) try: query_params = st.experimental_get_query_params() if 'code' in query_params and not st.session_state.spotify_token: auth_code = query_params['code'][0] st.info("🔄 Processing authentication...") token = exchange_code_for_token(auth_code) if token: st.session_state.spotify_token = token try: sp = spotipy.Spotify(auth=token) user_info = sp.current_user() st.session_state.user_info = { 'display_name': user_info.get('display_name', 'User'), 'id': user_info['id'] } st.success("✅ Successfully connected to Spotify!") st.experimental_set_query_params() time.sleep(2) st.rerun() except Exception as e: st.error(f"Failed to get user info: {e}") except Exception as e: pass if not st.session_state.spotify_token: # Authentication flow st.markdown("---") col1, col2, col3 = st.columns([1, 2, 1]) with col2: if st.session_state.auth_step == 'start': st.markdown("""Create custom playlists with genre and decade filtering!
Premium accounts get full song playback!
Copy this link and open it in a new browser tab:
Method: {method_text}
Tracks: {playlist['track_count']} songs
{"🚧 Demo Mode
" if playlist.get('is_demo') else "✅ Saved to Spotify!
"}{current_track['name']} by {current_track['artist']}
Track {st.session_state.current_track_index + 1} of {len(playlist['tracks'])}