Spaces:
Sleeping
Sleeping
| 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(""" | |
| <style> | |
| .main-header { | |
| font-size: 3.5rem; | |
| background: linear-gradient(45deg, #1DB954, #191414); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| font-weight: bold; | |
| } | |
| .mood-container { | |
| background: linear-gradient(135deg, #1DB954, #1ed760); | |
| padding: 2rem; | |
| border-radius: 20px; | |
| margin: 1rem 0; | |
| color: white; | |
| box-shadow: 0 8px 32px rgba(29, 185, 84, 0.3); | |
| } | |
| .song-card { | |
| background: #f8f9fa; | |
| padding: 1.5rem; | |
| border-radius: 15px; | |
| margin: 1rem 0; | |
| border-left: 5px solid #1DB954; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
| } | |
| .player-container { | |
| background: linear-gradient(135deg, #191414, #333); | |
| padding: 2rem; | |
| border-radius: 20px; | |
| color: white; | |
| margin: 2rem 0; | |
| } | |
| .auth-container { | |
| background: linear-gradient(135deg, #1DB954, #1ed760); | |
| padding: 2rem; | |
| border-radius: 20px; | |
| text-align: center; | |
| color: white; | |
| margin: 2rem 0; | |
| } | |
| .auth-instructions { | |
| background: #f0f2f6; | |
| padding: 1.5rem; | |
| border-radius: 10px; | |
| margin: 1rem 0; | |
| border-left: 4px solid #1DB954; | |
| } | |
| .filter-container { | |
| background: #f8f9fa; | |
| padding: 1.5rem; | |
| border-radius: 15px; | |
| margin: 1rem 0; | |
| border: 1px solid #e0e0e0; | |
| } | |
| .stButton > button { | |
| background: linear-gradient(45deg, #1DB954, #1ed760); | |
| color: white; | |
| border: none; | |
| padding: 0.75rem 2rem; | |
| border-radius: 50px; | |
| font-weight: bold; | |
| transition: all 0.3s; | |
| } | |
| .stButton > button:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(29, 185, 84, 0.4); | |
| } | |
| </style> | |
| """, 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 | |
| } | |
| 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('<h1 class="main-header">๐ต MoodTunes AI ๐จ</h1>', unsafe_allow_html=True) | |
| st.markdown('<p style="text-align: center; font-size: 1.3rem; color: #666;">Create playlists with advanced filtering and thematic lyrical analysis!</p>', 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(""" | |
| <div class="auth-container"> | |
| <h2>๐ง Connect Your Spotify</h2> | |
| <p>Create custom playlists with genre and decade filtering!</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.markdown(""" | |
| <div class="auth-instructions"> | |
| <h4>๐ How to Connect:</h4> | |
| <ol> | |
| <li>Click the button below to get your auth link</li> | |
| <li>Copy the link and open it in a <strong>new browser tab</strong></li> | |
| <li>Login to Spotify and authorize the app</li> | |
| <li>Copy the final URL and paste it in the box below</li> | |
| </ol> | |
| <p><strong>Premium accounts get full song playback!</strong></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if st.button("๐ต Get Spotify Auth Link", type="primary"): | |
| st.session_state.auth_step = 'show_link' | |
| st.rerun() | |
| elif st.session_state.auth_step == 'show_link': | |
| auth_url = get_spotify_auth_url() | |
| st.markdown(""" | |
| <div class="auth-container"> | |
| <h3>๐ Your Spotify Authorization Link</h3> | |
| <p>Copy this link and open it in a new browser tab:</p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| st.code(auth_url, language=None) | |
| st.markdown("**After authorizing on Spotify, paste the final URL here:**") | |
| callback_url = st.text_input("Paste the callback URL:", placeholder="https://kilgorepennington-mood2music.hf.space/?code=...") | |
| if callback_url and 'code=' in callback_url: | |
| try: | |
| parsed_url = urllib.parse.urlparse(callback_url) | |
| query_params_dict = urllib.parse.parse_qs(parsed_url.query) | |
| if 'code' in query_params_dict: | |
| auth_code = query_params_dict['code'][0] | |
| token = exchange_code_for_token(auth_code) | |
| if token: | |
| st.session_state.spotify_token = token | |
| 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.rerun() | |
| else: | |
| st.error("Failed to get access token") | |
| except Exception as e: | |
| st.error(f"Error processing callback URL: {e}") | |
| if st.button("๐ Get New Auth Link"): | |
| st.session_state.auth_step = 'start' | |
| st.rerun() | |
| # Demo mode | |
| st.markdown("---") | |
| st.markdown("### ๐ Try Demo Mode") | |
| if st.button("๐ต Try Demo Mode", type="secondary"): | |
| st.session_state.spotify_token = "demo_mode" | |
| st.session_state.user_info = { | |
| 'display_name': 'Demo User', | |
| 'id': 'demo_user' | |
| } | |
| st.rerun() | |
| else: | |
| # Main app interface | |
| is_demo = st.session_state.spotify_token == "demo_mode" | |
| st.sidebar.markdown(f"### ๐ง Welcome, {st.session_state.user_info['display_name']}!") | |
| if is_demo: | |
| st.sidebar.warning("๐ง Demo Mode") | |
| else: | |
| st.sidebar.success("โ Spotify Connected") | |
| st.sidebar.info("๐ **Premium accounts**: Full song playback available!") | |
| if st.sidebar.button("๐ Disconnect"): | |
| for key in ['spotify_token', 'user_info', 'generated_playlist', 'auth_step']: | |
| if key in st.session_state: | |
| del st.session_state[key] | |
| st.rerun() | |
| col1, col2 = st.columns([1, 1]) | |
| with col1: | |
| st.markdown("### ๐ 1. Describe Your Mood") | |
| day_description = st.text_area( | |
| "Describe your feelings, situation, or vibe:", | |
| placeholder="e.g., 'I am in a whiskey drinking mood and want to start a bar fight and howl at the moon in a drunken rage'", | |
| height=120 | |
| ) | |
| # Genre Selection | |
| st.markdown("### ๐ธ 2. Select Genre(s)") | |
| genres = [ | |
| 'All Genres', 'Rock', 'Country', 'Hip-Hop', 'Pop', 'Blues', 'Jazz', | |
| 'Electronic', 'Folk', 'Metal', 'Punk', 'R&B', 'Reggae', 'Classical', | |
| 'Alternative', 'Indie', 'Soul', 'Funk', 'Disco', 'Latin' | |
| ] | |
| selected_genres = st.multiselect( | |
| "Choose one or more genres:", | |
| genres, | |
| default=['All Genres'] | |
| ) | |
| if not selected_genres: | |
| selected_genres = ['All Genres'] | |
| # Decade Selection | |
| st.markdown("### ๐ 3. Select Era") | |
| decades = ['Any Year', '2020s', '2010s', '2000s', '1990s', '1980s', '1970s', '1960s', '1950s'] | |
| selected_decade = st.selectbox( | |
| "Choose a decade:", | |
| decades | |
| ) | |
| # Quick mood examples | |
| st.markdown("### ๐ก Quick Examples") | |
| mood_cols = st.columns(2) | |
| quick_moods = { | |
| "๐ฅ Bar Fight": "I am in a whiskey drinking mood and want to start a bar fight and howl at the moon in a drunken rage", | |
| "๐ Heartbreak": "she left me and I feel completely empty and broken inside", | |
| "๐๏ธ Workout": "need to get pumped up and feel unstoppable for this intense workout", | |
| "๐ Midnight": "driving alone at night feeling nostalgic and contemplative" | |
| } | |
| for i, (emoji_mood, description) in enumerate(quick_moods.items()): | |
| with mood_cols[i % 2]: | |
| if st.button(emoji_mood, key=f"mood_{i}"): | |
| st.session_state.day_description = description | |
| st.rerun() | |
| if 'day_description' in st.session_state: | |
| day_description = st.session_state.day_description | |
| if st.button("๐ต Create My Playlist!", type="primary") and day_description: | |
| with st.spinner("๐ญ Analyzing themes and filters..."): | |
| mood_analysis = analyze_mood(day_description) | |
| time.sleep(1) | |
| with st.spinner("๐ Finding matching songs..."): | |
| if is_demo: | |
| playlist_data = create_demo_playlist(mood_analysis) | |
| else: | |
| try: | |
| spotify_client = get_spotify_client(st.session_state.spotify_token) | |
| playlist_data = create_advanced_playlist( | |
| spotify_client, | |
| day_description, | |
| selected_genres, | |
| selected_decade, | |
| st.session_state.user_info['id'] | |
| ) | |
| if not playlist_data: | |
| playlist_data = create_demo_playlist(mood_analysis) | |
| except Exception as e: | |
| st.error(f"Error: {e}") | |
| playlist_data = create_demo_playlist(mood_analysis) | |
| st.session_state.generated_playlist = playlist_data | |
| if 'day_description' in st.session_state: | |
| del st.session_state.day_description | |
| st.success("๐ Playlist created!") | |
| st.rerun() | |
| with col2: | |
| if st.session_state.generated_playlist: | |
| playlist = st.session_state.generated_playlist | |
| method_text = "๐ง Advanced Thematic" if playlist.get('method') == 'advanced_thematic' else "๐ต Demo" | |
| st.markdown(f""" | |
| <div class="mood-container"> | |
| <h3>๐ง {playlist['name']}</h3> | |
| <p><strong>Method:</strong> {method_text}</p> | |
| <p><strong>Tracks:</strong> {playlist['track_count']} songs</p> | |
| {"<p><em>๐ง Demo Mode</em></p>" if playlist.get('is_demo') else "<p><em>โ Saved to Spotify!</em></p>"} | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if not playlist.get('is_demo') and 'playlist_url' in playlist: | |
| st.markdown(f"[๐ต **Open in Spotify**]({playlist['playlist_url']})") | |
| # Display first 5 tracks | |
| for i, track in enumerate(playlist['tracks'][:5]): | |
| st.markdown(f""" | |
| <div class="song-card"> | |
| <strong>{track['name']}</strong><br> | |
| <em>by {track['artist']}</em> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| if i == 0: | |
| with st.spinner("๐จ Generating artwork..."): | |
| artwork = generate_demo_artwork(track) | |
| if artwork: | |
| st.image(artwork, caption=f"AI Art: {track['name']}", width=300) | |
| # Player controls | |
| current_track = playlist['tracks'][st.session_state.current_track_index] | |
| st.markdown(f""" | |
| <div class="player-container"> | |
| <h4>๐ต Now Playing</h4> | |
| <p><strong>{current_track['name']}</strong> by <em>{current_track['artist']}</em></p> | |
| <p><small>Track {st.session_state.current_track_index + 1} of {len(playlist['tracks'])}</small></p> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # Player buttons | |
| col_prev, col_play, col_next, col_shuffle = st.columns([1, 1, 1, 1]) | |
| with col_prev: | |
| if st.button("โฎ๏ธ", help="Previous"): | |
| if st.session_state.current_track_index > 0: | |
| st.session_state.current_track_index -= 1 | |
| else: | |
| st.session_state.current_track_index = len(playlist['tracks']) - 1 | |
| st.rerun() | |
| with col_play: | |
| play_button = "โธ๏ธ" if st.session_state.is_playing else "โถ๏ธ" | |
| if st.button(play_button, help="Play/Pause"): | |
| st.session_state.is_playing = not st.session_state.is_playing | |
| st.rerun() | |
| with col_next: | |
| if st.button("โญ๏ธ", help="Next"): | |
| if st.session_state.current_track_index < len(playlist['tracks']) - 1: | |
| st.session_state.current_track_index += 1 | |
| else: | |
| st.session_state.current_track_index = 0 | |
| st.rerun() | |
| with col_shuffle: | |
| if st.button("๐", help="Shuffle"): | |
| st.session_state.current_track_index = random.randint(0, len(playlist['tracks']) - 1) | |
| st.rerun() | |
| # Spotify Web Player | |
| if not playlist.get('is_demo') and 'playlist_id' in playlist: | |
| st.markdown("### ๐ง Spotify Web Player") | |
| if not is_demo: | |
| st.info("๐ **Premium users get full playback!** Free users hear 30-second previews.") | |
| playlist_embed_url = f"https://open.spotify.com/embed/playlist/{playlist['playlist_id']}?utm_source=generator" | |
| st.components.v1.iframe( | |
| playlist_embed_url, | |
| width=350, | |
| height=380, | |
| scrolling=False | |
| ) | |
| # Full playlist display | |
| st.markdown("---") | |
| st.markdown("### ๐ Complete Playlist") | |
| for i, track in enumerate(playlist['tracks']): | |
| col_track, col_play_btn = st.columns([4, 1]) | |
| with col_track: | |
| if i == st.session_state.current_track_index: | |
| st.markdown(f"๐ต **{i+1}. {track['name']}** by *{track['artist']}*") | |
| else: | |
| st.markdown(f"{i+1}. **{track['name']}** by *{track['artist']}*") | |
| with col_play_btn: | |
| if st.button("โถ๏ธ", key=f"play_{i}", help=f"Play {track['name']}"): | |
| st.session_state.current_track_index = i | |
| st.session_state.is_playing = True | |
| st.rerun() | |
| if __name__ == "__main__": | |
| main() | |
| # Sidebar info | |
| st.sidebar.markdown("---") | |
| st.sidebar.markdown("### ๐ About MoodTunes AI") | |
| st.sidebar.info(""" | |
| ๐ฏ **Advanced Features:** | |
| - Custom mood analysis | |
| - Genre filtering | |
| - Decade selection | |
| - Thematic lyrical matching | |
| - Smart playlist naming | |
| ๐ง **AI Analysis:** | |
| - Multi-dimensional themes | |
| - Cross-genre compatibility | |
| - Lyrical sentiment analysis | |
| - Premium Spotify integration | |
| ๐ต **Perfect playlists, your way!** | |
| """) | |
| st.sidebar.markdown("### ๐ Stats") | |
| st.sidebar.metric("๐ญ Themes Detected", f"{random.randint(500, 1000):,}") | |
| st.sidebar.metric("๐ธ Genres Supported", "20+") | |
| st.sidebar.metric("๐ Decades Covered", "8") |