import os import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go from stravalib.client import Client import google.generativeai as genai from dotenv import load_dotenv from datetime import datetime, timedelta # Load environment variables load_dotenv() # API Keys and Secrets STRAVA_CLIENT_ID = os.getenv('STRAVA_CLIENT_ID') STRAVA_CLIENT_SECRET = os.getenv('STRAVA_CLIENT_SECRET') GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') # Strava API setup client = Client() # Gemini API setup genai.configure(api_key=GEMINI_API_KEY) model = genai.GenerativeModel('gemini-pro') # Streamlit app st.set_page_config(page_title="Strava Run Analysis", layout="wide") # Custom CSS for better UI st.markdown(""" """, unsafe_allow_html=True) st.title("🏃‍♂️ Strava Run Analysis") # Strava manual authentication if 'access_token' not in st.session_state: st.write("To use this app, you need to authorize it with Strava. Follow these steps:") st.write("1. Click the button below to go to Strava's authorization page:") auth_url = f"https://www.strava.com/oauth/authorize?client_id={STRAVA_CLIENT_ID}&response_type=code&redirect_uri=http://localhost&approval_prompt=force&scope=read_all,profile:read_all,activity:read_all" st.markdown(f"", unsafe_allow_html=True) st.write("2. Log in to Strava if needed and click 'Authorize'") st.write("3. After authorizing, you'll be redirected to a page that may show an error. This is expected!") st.write("4. Copy the 'code' parameter from the URL of that page.") st.write("5. Paste that code below:") auth_code = st.text_input("Paste the authorization code here:") if auth_code: try: token_response = client.exchange_code_for_token( client_id=STRAVA_CLIENT_ID, client_secret=STRAVA_CLIENT_SECRET, code=auth_code ) st.session_state.access_token = token_response['access_token'] st.success("Authorization successful! Refreshing the app...") st.rerun() except Exception as e: st.error(f"An error occurred: {str(e)}. Please try authorizing again.") if 'access_token' in st.session_state: client.access_token = st.session_state.access_token try: athlete = client.get_athlete() st.write(f"Welcome, {athlete.firstname} {athlete.lastname}! 👋") except Exception as e: st.error(f"Error fetching athlete data: {str(e)}. Please try reauthorizing.") if st.button("Reauthorize"): del st.session_state.access_token st.rerun() # Fetch running activities @st.cache_data(ttl=3600) def fetch_run_activities(): activities = list(client.get_activities(limit=200)) runs = [] for activity in activities: if activity.type == 'Run': run_data = { 'name': activity.name, 'distance': float(activity.distance.num) / 1000 if activity.distance else None, # Convert to km 'moving_time': None, 'total_elevation_gain': float(activity.total_elevation_gain) if activity.total_elevation_gain else None, 'average_speed': float(activity.average_speed) * 3.6 if activity.average_speed else None, # Convert to km/h 'average_heartrate': float(activity.average_heartrate) if activity.average_heartrate else None, 'start_date': activity.start_date.replace(tzinfo=None) if activity.start_date else None } # Safely handle moving_time try: if activity.moving_time: run_data['moving_time'] = activity.moving_time.total_seconds() / 60 # Convert to minutes except AttributeError: pass # If moving_time is not accessible, leave it as None runs.append(run_data) df = pd.DataFrame(runs) # Calculate pace only if both moving_time and distance are available df['pace'] = df.apply(lambda row: row['moving_time'] / row['distance'] if row['moving_time'] and row['distance'] else None, axis=1) return df try: df = fetch_run_activities() except Exception as e: st.error(f"Error fetching activities: {str(e)}. Please try again later.") st.stop() # Basic stats st.markdown("

📊 Run Statistics

", unsafe_allow_html=True) col1, col2, col3, col4 = st.columns(4) with col1: st.markdown("

{:.0f}

Total Runs

".format(len(df)), unsafe_allow_html=True) with col2: st.markdown("

{:.0f} km

Total Distance

".format(df['distance'].sum()), unsafe_allow_html=True) with col3: total_time = df['moving_time'].sum() st.markdown("

{:.0f}h {:.0f}m

Total Time

".format(total_time // 60, total_time % 60), unsafe_allow_html=True) with col4: st.markdown("

{:.0f} m

Total Elevation Gain

".format(df['total_elevation_gain'].sum()), unsafe_allow_html=True) # Visualizations st.markdown("

📈 Run Analysis

", unsafe_allow_html=True) # Weekly distance weekly_distance = df.resample('W', on='start_date')['distance'].sum().reset_index() fig_weekly = px.bar(weekly_distance, x='start_date', y='distance', title="Weekly Running Distance", labels={'distance': 'Distance (km)', 'start_date': 'Week'}) fig_weekly.update_layout(xaxis_title="Week", yaxis_title="Distance (km)") st.plotly_chart(fig_weekly, use_container_width=True) # Pace improvement df_sorted = df.sort_values('start_date') fig_pace = px.scatter(df_sorted, x='start_date', y='pace', title="Running Pace Over Time", labels={'pace': 'Pace (min/km)', 'start_date': 'Date'}) fig_pace.add_trace(go.Scatter(x=df_sorted['start_date'], y=df_sorted['pace'].rolling(window=10).mean(), mode='lines', name='10-run moving average')) fig_pace.update_layout(yaxis_title="Pace (min/km)") st.plotly_chart(fig_pace, use_container_width=True) # Heart rate improvement (if data available) if df['average_heartrate'].notna().any(): df_hr = df_sorted[df_sorted['average_heartrate'].notna()] fig_hr = px.scatter(df_hr, x='start_date', y='average_heartrate', title="Average Heart Rate Over Time", labels={'average_heartrate': 'Average Heart Rate (bpm)', 'start_date': 'Date'}) fig_hr.add_trace(go.Scatter(x=df_hr['start_date'], y=df_hr['average_heartrate'].rolling(window=10).mean(), mode='lines', name='10-run moving average')) fig_hr.update_layout(yaxis_title="Average Heart Rate (bpm)") st.plotly_chart(fig_hr, use_container_width=True) else: st.info("No heart rate data available for analysis.") # Distance vs. Elevation gain fig_elev = px.scatter(df, x='distance', y='total_elevation_gain', title="Distance vs. Elevation Gain", labels={'distance': 'Distance (km)', 'total_elevation_gain': 'Elevation Gain (m)'}) fig_elev.update_layout(xaxis_title="Distance (km)", yaxis_title="Elevation Gain (m)") st.plotly_chart(fig_elev, use_container_width=True) # Training Plan Generation st.markdown("

🏋️‍♀️ Generate Training Plan

", unsafe_allow_html=True) col1, col2, col3 = st.columns(3) with col1: level = st.selectbox("Select your level:", ["Beginner", "Intermediate", "Advanced"]) with col2: race_distance = st.selectbox("Select race distance:", ["5K", "10K", "Half Marathon", "Marathon"]) with col3: plan_duration = st.selectbox("Select plan duration:", ["8 weeks", "10 weeks", "12 weeks"]) if st.button("Generate Training Plan"): with st.spinner("Generating your personalized training plan..."): try: prompt = f"""Create a {plan_duration} training plan for a {level} runner preparing for a {race_distance} race. Include weekly mileage and key workouts. Format the plan week by week, with each week on a new line. Consider the runner's current weekly mileage of {weekly_distance['distance'].iloc[-1]:.1f} km.""" response = model.generate_content(prompt) st.markdown(response.text) except Exception as e: st.error(f"Error generating training plan: {str(e)}. Please try again later.") else: st.info("Please complete the authorization process to view your Strava running data and analytics.")