RosterHash/app.py
Eduardo_Figueroa ca702d6bcc fake a username
2025-10-31 11:43:36 -07:00

462 lines
22 KiB
Python

from flask import Flask, render_template, jsonify, request, session, redirect, url_for, send_file, make_response
from datetime import datetime, timedelta
import os
import sys
import traceback
from services.sleeper_api import SleeperAPI
from services.espn_api import ESPNAPI
from config import Config
from faker import Faker
# Force unbuffered output for Docker logs
os.environ['PYTHONUNBUFFERED'] = '1'
print("=== RosterHash Starting ===", flush=True)
print(f"=== Version: {Config.VERSION} ===", flush=True)
app = Flask(__name__)
app.config.from_object(Config)
# Initialize API services
sleeper_api = SleeperAPI()
espn_api = ESPNAPI()
# Simple memory cache for API responses
api_cache = {}
CACHE_DURATION = 300 # 5 minutes in seconds
# Debug time override
debug_time_override = None
def get_debug_time():
"""Get the current time, or debug override time if set"""
global debug_time_override
if debug_time_override:
return debug_time_override
return datetime.now()
def get_cached_or_fetch(cache_key, fetch_function, *args):
"""Get data from cache or fetch it and cache the result"""
now = get_debug_time()
if cache_key in api_cache:
cached_data, cached_time = api_cache[cache_key]
if (now - cached_time).total_seconds() < CACHE_DURATION:
print(f"DEBUG: Using cached data for {cache_key}", flush=True)
return cached_data
print(f"DEBUG: Fetching fresh data for {cache_key}", flush=True)
try:
data = fetch_function(*args)
api_cache[cache_key] = (data, now)
return data
except Exception as e:
print(f"ERROR: Failed to fetch {cache_key}: {str(e)}", flush=True)
# Return cached data if available, even if stale
if cache_key in api_cache:
cached_data, _ = api_cache[cache_key]
print(f"DEBUG: Using stale cached data for {cache_key}", flush=True)
return cached_data
raise
def get_league_color(league_index):
"""Assign colors to leagues in order"""
colors = app.config['LEAGUE_COLORS']
return colors[league_index % len(colors)] # Cycle through colors
def normalize_team_abbreviation(espn_team):
"""Convert ESPN team abbreviation to Sleeper format"""
team_map = app.config['TEAM_ABBREVIATION_MAP']
return team_map.get(espn_team, espn_team)
@app.context_processor
def inject_apis():
"""Make API and version available to all templates"""
global debug_time_override
is_debug_mode = request.args.get('debug') == 'true'
return dict(
sleeper_api=sleeper_api,
app_version=app.config['VERSION'],
current_theme=session.get('theme', 'dark'),
is_debug_mode=is_debug_mode,
debug_time_override=debug_time_override
)
@app.route('/')
def index():
"""Home page with username input"""
print("DEBUG: Index route accessed", flush=True)
# Get the last used username from cookie
last_username = request.cookies.get('last_username', '')
mySleeperUsername = Faker().user_name()
print(f"DEBUG: Last username from cookie: '{last_username}'", flush=True)
# Auto-redirect to dashboard if username cookie exists
if last_username and last_username.strip():
print(f"DEBUG: Auto-redirecting to dashboard for user: '{last_username}'", flush=True)
return redirect(url_for('dashboard_current', username=last_username.strip()))
return render_template('index.html', last_username=last_username, mySleeperUsername=mySleeperUsername)
@app.route('/dashboard')
def dashboard_form():
"""Handle form submission from homepage"""
username = request.args.get('username')
if username:
return dashboard_current(username.strip())
else:
return redirect(url_for('index'))
# Add theme handling route
@app.route('/toggle_theme', methods=['POST'])
def toggle_theme():
"""Toggle between light and dark theme"""
current_theme = session.get('theme', 'dark')
new_theme = 'dark' if current_theme == 'light' else 'light'
session['theme'] = new_theme
# Get return URL from form or default to index
return_url = request.form.get('return_url', url_for('index'))
return redirect(return_url)
# Add timezone handling route
@app.route('/set_timezone', methods=['POST'])
def set_timezone():
"""Set user's timezone preference"""
timezone = request.form.get('timezone')
if timezone:
session['user_timezone'] = timezone
print(f"DEBUG: Set user timezone to: {timezone}", flush=True)
# Get return URL from form or default to index
return_url = request.form.get('return_url', url_for('index'))
return redirect(return_url)
# Add debug time override route
@app.route('/set_debug_time', methods=['POST'])
def set_debug_time():
"""Set debug time override (only in debug mode)"""
global debug_time_override
# Check if debug mode is enabled
if not request.args.get('debug') == 'true':
return jsonify({'error': 'Debug mode not enabled'}), 403
debug_datetime_str = request.form.get('debug_time')
if debug_datetime_str:
try:
# Parse the datetime string from the picker (format: YYYY-MM-DDTHH:MM)
debug_time_override = datetime.strptime(debug_datetime_str, '%Y-%m-%dT%H:%M')
print(f"DEBUG: Set debug time override to: {debug_time_override}", flush=True)
# Clear cache when time changes to get fresh data
api_cache.clear()
print("DEBUG: Cleared API cache due to time override", flush=True)
except ValueError as e:
print(f"DEBUG: Invalid debug time format: {debug_datetime_str}, error: {e}", flush=True)
return jsonify({'error': 'Invalid datetime format'}), 400
else:
# Clear the override
debug_time_override = None
api_cache.clear()
print("DEBUG: Cleared debug time override", flush=True)
# Get return URL from form or default to index
return_url = request.form.get('return_url', url_for('index'))
return redirect(return_url)
@app.route('/change-username')
def change_username():
"""Clear username cookie and redirect to index"""
response = make_response(redirect(url_for('index')))
response.set_cookie('last_username', '', expires=0) # Clear the cookie
return response
@app.route('/<username>')
def dashboard_current(username):
"""Dashboard for current NFL week"""
print(f"DEBUG: Dashboard current route - username: '{username}'", flush=True)
try:
nfl_state = sleeper_api.get_nfl_state()
current_week = nfl_state.get('week', 1)
print(f"DEBUG: Current week: {current_week}", flush=True)
response = make_response(dashboard(username, current_week))
# Set cookie to remember this username (expires in 30 days)
response.set_cookie('last_username', username, max_age=30*24*60*60)
return response
except Exception as e:
print(f"ERROR: dashboard_current exception - {str(e)}", flush=True)
print(f"ERROR: Full traceback: {traceback.format_exc()}", flush=True)
return render_template('error.html',
message=f"Could not find user '{username}'. Please check the username and try again.")
@app.route('/<username>/<int:week>')
def dashboard_week(username, week):
"""Dashboard for specific week"""
print(f"DEBUG: Dashboard week - username: '{username}', week: {week}", flush=True)
response = make_response(dashboard(username, week))
# Set cookie to remember this username (expires in 30 days)
response.set_cookie('last_username', username, max_age=30*24*60*60)
return response
@app.route('/<username>/<int:week>/refresh', methods=['POST'])
def refresh_scores(username, week):
"""Server-side refresh scores - redirect back to dashboard"""
print(f"DEBUG: Refresh scores POST - username: '{username}', week: {week}", flush=True)
return redirect(url_for('dashboard_week', username=username, week=week))
def dashboard(username, week):
"""Main dashboard logic - fetch and display user's fantasy data"""
print(f"DEBUG: Dashboard function START - username: '{username}', week: {week}", flush=True)
try:
# Get user info from Sleeper (cached)
user_cache_key = f"user_{username}"
user = get_cached_or_fetch(user_cache_key, sleeper_api.get_user, username)
print(f"DEBUG: get_user() returned: {user}", flush=True)
if not user:
print(f"WARNING: User lookup failed for username: '{username}'", flush=True)
return render_template('error.html',
message=f"Could not find user '{username}'. Please check the username and try again.")
print(f"DEBUG: User found - ID: {user.get('user_id')}, Display: {user.get('display_name', 'Unknown')}", flush=True)
# Get current NFL season (cached)
nfl_cache_key = "nfl_state"
nfl_state = get_cached_or_fetch(nfl_cache_key, sleeper_api.get_nfl_state)
season = nfl_state.get('season', str(datetime.now().year))
print(f"DEBUG: Using season: {season}", flush=True)
# Get user's fantasy leagues (cached)
user_id = user['user_id']
leagues_cache_key = f"leagues_{user_id}_{season}"
leagues = get_cached_or_fetch(leagues_cache_key, sleeper_api.get_user_leagues, user_id, season)
print(f"DEBUG: Found {len(leagues) if leagues else 0} leagues", flush=True)
if leagues:
for i, league in enumerate(leagues):
print(f"DEBUG: League {i+1}: ID={league.get('league_id')}, Name='{league.get('name')}'", flush=True)
# Get NFL game schedule for the week with user's timezone
user_timezone = session.get('user_timezone', 'America/Los_Angeles') # Default to PST
print(f"DEBUG: Using timezone: {user_timezone}", flush=True)
print(f"DEBUG: Calling get_week_schedule() for week {week}, season {season}", flush=True)
try:
schedule = espn_api.get_week_schedule(week, season, user_timezone, debug_time_override)
print(f"DEBUG: Schedule retrieved successfully", flush=True)
except Exception as e:
print(f"ERROR: ESPN schedule fetch failed: {str(e)}", flush=True)
schedule = {}
# Process each league for matchup data
league_data = []
for i, league in enumerate(leagues):
print(f"DEBUG: ===== Processing league {i+1}/{len(leagues)}: '{league['name']}' =====", flush=True)
league_id = league['league_id']
try:
# Get matchups for the current week
print(f"DEBUG: Calling get_matchups() for league {league_id}, week {week}", flush=True)
matchups = sleeper_api.get_matchups(league_id, week)
print(f"DEBUG: Found {len(matchups) if matchups else 0} matchups", flush=True)
# Get rosters to find user's team
print(f"DEBUG: Calling get_rosters() for league {league_id}", flush=True)
rosters = sleeper_api.get_rosters(league_id)
print(f"DEBUG: Found {len(rosters) if rosters else 0} rosters", flush=True)
# Find user's roster in this league
user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None)
print(f"DEBUG: User roster found: {user_roster is not None}", flush=True)
if user_roster:
# Find user's matchup for this week (if matchups exist)
user_matchup = None
if matchups:
user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None)
print(f"DEBUG: User matchup found: {user_matchup is not None}", flush=True)
# Find opponent's matchup and user info
opponent_matchup = None
opponent_user = None
if user_matchup and matchups:
# Find opponent in same matchup
opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None)
print(f"DEBUG: Opponent matchup found: {opponent_matchup is not None}", flush=True)
if opponent_matchup:
# Get opponent's roster to find owner
opponent_roster = next((r for r in rosters if r['roster_id'] == opponent_matchup['roster_id']), None)
if opponent_roster:
opponent_owner_id = opponent_roster['owner_id']
# Fetch opponent's user profile
opponent_user = sleeper_api.get_user_by_id(opponent_owner_id)
print(f"DEBUG: Opponent user: {opponent_user.get('display_name') if opponent_user else 'None'}", flush=True)
# Get all players on user's roster for calendar with starter info
all_players = []
if user_roster and user_roster.get('players'):
players_list = user_roster['players']
starters_list = user_matchup.get('starters') if user_matchup else None
starters_list = starters_list or [] # Handle None case
players_points = user_matchup.get('players_points') if user_matchup else None
players_points = players_points or {} # Handle None case
print(f"DEBUG: Processing {len(players_list)} total players, {len(starters_list)} starters", flush=True)
for player_id in players_list:
try:
# Get player details from Sleeper API
player_info = sleeper_api.get_player_info(player_id)
if player_info:
# Create a copy of the player object for this league to avoid shared references
player = player_info.copy()
# Add starter status to player data
player['is_starter'] = player_id in starters_list
# Add player points for this week (league-specific)
player['points'] = players_points.get(player_id, 0.0)
# Check multiple possible injury status fields
injury_status = (
player.get('injury_status') or
player.get('status') or
player.get('injury_designation') or
player.get('practice_participation')
)
# Only show injury status if it's not "Active" or None
if injury_status and injury_status.lower() != 'active':
player['injury_status'] = injury_status
else:
player['injury_status'] = None
# Print any player with injury status (only non-Active)
if player['injury_status']:
player_name = f"{player.get('first_name', '')} {player.get('last_name', '')}".strip()
print(f"DEBUG: {player_name} has injury status: {player['injury_status']}", flush=True)
all_players.append(player)
except Exception as e:
print(f"ERROR: Failed to get player info for {player_id}: {str(e)}", flush=True)
# Calculate winning/losing status
user_points = user_matchup['points'] if user_matchup else 0
opponent_points = opponent_matchup['points'] if opponent_matchup else 0
# Determine status
if user_points > opponent_points:
match_status = 'winning'
elif user_points < opponent_points:
match_status = 'losing'
else:
match_status = 'tied'
# Store processed league data
league_info = {
'league': league,
'league_color': get_league_color(i), # Assign color by order
'user_matchup': user_matchup,
'opponent_matchup': opponent_matchup,
'opponent_user': opponent_user,
'user_roster': user_roster,
'all_players': all_players,
'match_status': match_status,
'user_points': user_points,
'opponent_points': opponent_points
}
league_data.append(league_info)
print(f"DEBUG: League '{league['name']}' processed successfully", flush=True)
else:
print(f"DEBUG: Skipping league '{league['name']}' - missing data", flush=True)
except Exception as e:
print(f"ERROR: Failed to process league '{league['name']}': {str(e)}", flush=True)
print(f"ERROR: League processing traceback: {traceback.format_exc()}", flush=True)
# Debug output before rendering template
print(f"DEBUG: About to render template with {len(league_data)} leagues", flush=True)
for league_info in league_data:
print(f"DEBUG: League '{league_info['league']['name']}' has {len(league_info['all_players'])} players", flush=True)
total_game_count = sum(len(games) for games in schedule.values())
print(f"DEBUG: Schedule has {total_game_count} total games", flush=True)
try:
# Render dashboard with all collected data
result = render_template('dashboard.html',
user=user,
week=week,
league_data=league_data,
schedule=schedule,
nfl_state=nfl_state)
print(f"DEBUG: Template rendered successfully", flush=True)
return result
except Exception as e:
print(f"ERROR: Template rendering failed: {str(e)}", flush=True)
print(f"ERROR: Template rendering traceback: {traceback.format_exc()}", flush=True)
return render_template('error.html', message=f"Template error: {str(e)}")
except Exception as e:
print(f"ERROR: Dashboard function exception: {str(e)}", flush=True)
print(f"ERROR: Full dashboard traceback: {traceback.format_exc()}", flush=True)
return render_template('error.html',
message=f"Error loading data: {str(e)}")
@app.route('/api/refresh/<username>/<int:week>')
def refresh_data(username, week):
"""API endpoint for live score updates"""
print(f"DEBUG: Refresh data API - username: '{username}', week: {week}", flush=True)
try:
# Get user and current season
user = sleeper_api.get_user(username)
if not user:
return jsonify({'error': 'User not found'}), 404
nfl_state = sleeper_api.get_nfl_state()
season = nfl_state.get('season', str(datetime.now().year))
leagues = sleeper_api.get_user_leagues(user['user_id'], season)
league_scores = []
# Get updated scores for each league
for league in leagues:
matchups = sleeper_api.get_matchups(league['league_id'], week)
rosters = sleeper_api.get_rosters(league['league_id'])
user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None)
if user_roster and matchups:
# Find user and opponent matchups
user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None)
opponent_matchup = None
if user_matchup:
opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None)
# Return current points
league_scores.append({
'league_id': league['league_id'],
'league_name': league['name'],
'user_points': user_matchup['points'] if user_matchup else 0,
'opponent_points': opponent_matchup['points'] if opponent_matchup else 0
})
return jsonify({'league_scores': league_scores})
except Exception as e:
print(f"ERROR: Refresh data exception: {str(e)}", flush=True)
return jsonify({'error': str(e)}), 500
# Static file routes
@app.route('/rosterhash_logo.png')
def logo():
"""Serve the logo file"""
return send_file('rosterhash_logo.png', mimetype='image/png')
@app.route('/favicon.ico')
def favicon():
"""Serve the favicon"""
return send_file('rosterhash_logo.png', mimetype='image/png')
if __name__ == '__main__':
print("DEBUG: Starting Flask app on 0.0.0.0:5000", flush=True)
print(f"DEBUG: RosterHash {app.config['VERSION']} ready!", flush=True)
app.run(host='0.0.0.0', port=5000, debug=True)