StravaBot / app.py
Prashanthsrn's picture
Update app.py
03dc139 verified
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("""
<style>
.stApp {
background-color: #f0f2f6;
}
.stButton>button {
background-color: #fc4c02;
color: white;
font-weight: bold;
border-radius: 5px;
border: none;
padding: 0.5rem 1rem;
}
.stButton>button:hover {
background-color: #e34402;
}
.stSelectbox {
color: #333333;
}
.stPlotlyChart {
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 1.5rem;
text-align: center;
}
.stat-card h3 {
color: #fc4c02;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.stat-card p {
color: #666;
font-size: 1rem;
margin: 0;
}
.section-header {
color: #333;
font-size: 1.8rem;
font-weight: bold;
margin-top: 2rem;
margin-bottom: 1rem;
}
</style>
""", 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"<a href='{auth_url}' target='_blank'><button style='background-color: #fc4c02; color: white; padding: 0.5rem 1rem; border: none; border-radius: 5px; cursor: pointer;'>Authorize Strava</button></a>", 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("<h2 class='section-header'>πŸ“Š Run Statistics</h2>", unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.markdown("<div class='stat-card'><h3>{:.0f}</h3><p>Total Runs</p></div>".format(len(df)), unsafe_allow_html=True)
with col2:
st.markdown("<div class='stat-card'><h3>{:.0f} km</h3><p>Total Distance</p></div>".format(df['distance'].sum()), unsafe_allow_html=True)
with col3:
total_time = df['moving_time'].sum()
st.markdown("<div class='stat-card'><h3>{:.0f}h {:.0f}m</h3><p>Total Time</p></div>".format(total_time // 60, total_time % 60), unsafe_allow_html=True)
with col4:
st.markdown("<div class='stat-card'><h3>{:.0f} m</h3><p>Total Elevation Gain</p></div>".format(df['total_elevation_gain'].sum()), unsafe_allow_html=True)
# Visualizations
st.markdown("<h2 class='section-header'>πŸ“ˆ Run Analysis</h2>", 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("<h2 class='section-header'>πŸ‹οΈβ€β™€οΈ Generate Training Plan</h2>", 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.")