Player list. Mmh
Browse files
main.py
CHANGED
|
@@ -15,6 +15,7 @@ from firebase_admin import credentials, db, storage, auth
|
|
| 15 |
import firebase_admin
|
| 16 |
import logging
|
| 17 |
import traceback
|
|
|
|
| 18 |
from bs4 import BeautifulSoup, Comment
|
| 19 |
|
| 20 |
try:
|
|
@@ -240,7 +241,7 @@ def submit_feedback():
|
|
| 240 |
message = data.get('message')
|
| 241 |
|
| 242 |
if not feedback_type or not message:
|
| 243 |
-
return jsonify({'error': 'Feedback type and message
|
| 244 |
|
| 245 |
user_data = db.reference(f'users/{uid}').get()
|
| 246 |
user_email = user_data.get('email', 'unknown_email')
|
|
@@ -525,6 +526,13 @@ def admin_update_credits(uid):
|
|
| 525 |
# NBA Analytics Hub Data Fetching Utilities
|
| 526 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
def clean_firebase_keys(key_name):
|
| 529 |
if not isinstance(key_name, str):
|
| 530 |
key_name = str(key_name)
|
|
@@ -650,26 +658,39 @@ def get_player_index_brscraper():
|
|
| 650 |
return df
|
| 651 |
|
| 652 |
def _scrape_player_index_brscraper():
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
|
| 674 |
def get_player_career_stats_brscraper(player_name, seasons_to_check=10, playoffs=False):
|
| 675 |
if not BRSCRAPER_AVAILABLE:
|
|
@@ -677,37 +698,55 @@ def get_player_career_stats_brscraper(player_name, seasons_to_check=10, playoffs
|
|
| 677 |
return pd.DataFrame()
|
| 678 |
all_rows = []
|
| 679 |
|
|
|
|
|
|
|
| 680 |
|
| 681 |
-
seasons_to_try = get_available_seasons_util(seasons_to_check)
|
| 682 |
|
| 683 |
for season_str in seasons_to_try:
|
| 684 |
end_year = int(season_str.split('β')[1])
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
|
|
|
|
|
|
| 711 |
|
| 712 |
if not all_rows:
|
| 713 |
logging.warning(f"DEBUG: No stats found for {player_name} across all attempted seasons. Returning empty DataFrame.")
|
|
@@ -730,7 +769,7 @@ def get_player_career_stats_brscraper(player_name, seasons_to_check=10, playoffs
|
|
| 730 |
if col not in non_num:
|
| 731 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 732 |
|
| 733 |
-
df['Player'] = player_name
|
| 734 |
df = df.replace({np.nan: None})
|
| 735 |
return df
|
| 736 |
|
|
@@ -908,19 +947,14 @@ def get_player_stats():
|
|
| 908 |
all_player_season_data = []
|
| 909 |
players_with_no_data = []
|
| 910 |
|
| 911 |
-
# Iterate through each player requested
|
| 912 |
for player_name in selected_players:
|
| 913 |
-
# Call get_player_career_stats_brscraper to get all available seasons for this player
|
| 914 |
-
# This function will log warnings if specific seasons fail to fetch
|
| 915 |
df_player_career = get_player_career_stats_brscraper(player_name, playoffs=False)
|
| 916 |
|
| 917 |
if df_player_career.empty:
|
| 918 |
logging.info(f"No career data found for {player_name}. Adding to no_data list.")
|
| 919 |
players_with_no_data.append(player_name)
|
| 920 |
-
continue
|
| 921 |
|
| 922 |
-
# Filter the career data for the specific seasons requested in the frontend payload
|
| 923 |
-
# Note: selected_seasons here is the list of seasons for *all* players in the request
|
| 924 |
filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
|
| 925 |
|
| 926 |
if not filtered_df.empty:
|
|
@@ -1269,7 +1303,7 @@ def awards_predictor():
|
|
| 1269 |
return jsonify({'error': 'Award type and criteria are required'}), 400
|
| 1270 |
|
| 1271 |
prompt = f"Predict top 5 {award_type} candidates based on {criteria}. Focus on 2024-25 season."
|
| 1272 |
-
prediction = ask_perp(prompt
|
| 1273 |
if "Error from AI" in prediction:
|
| 1274 |
return jsonify({'error': prediction}), 500
|
| 1275 |
|
|
@@ -1302,7 +1336,7 @@ def young_player_projection():
|
|
| 1302 |
"4. Comparison to similar players at the same age. 5. Career trajectory prediction. "
|
| 1303 |
"Base your analysis on historical player development patterns and current NBA trends."
|
| 1304 |
)
|
| 1305 |
-
projection = ask_perp(prompt
|
| 1306 |
if "Error from AI" in projection:
|
| 1307 |
return jsonify({'error': projection}), 500
|
| 1308 |
|
|
@@ -1328,12 +1362,11 @@ def similar_players():
|
|
| 1328 |
if "Error from AI" in similar_players_analysis:
|
| 1329 |
return jsonify({'error': similar_players_analysis}), 500
|
| 1330 |
|
| 1331 |
-
# Extract user ID from auth header
|
| 1332 |
auth_header = request.headers.get('Authorization', '')
|
| 1333 |
token = auth_header.split(' ')[1]
|
| 1334 |
uid = verify_token(token)
|
| 1335 |
|
| 1336 |
-
analysis_id = str(uuid.uuid4())
|
| 1337 |
|
| 1338 |
if FIREBASE_INITIALIZED:
|
| 1339 |
user_analyses_ref = db.reference(f'user_analyses/{uid}')
|
|
@@ -1356,6 +1389,50 @@ def similar_players():
|
|
| 1356 |
logging.error(f"Error in /api/nba/similar_players: {e}")
|
| 1357 |
return jsonify({'error': str(e)}), 500
|
| 1358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1359 |
|
| 1360 |
if __name__ == '__main__':
|
| 1361 |
app.run(debug=True, host="0.0.0.0", port=7860)
|
|
|
|
| 15 |
import firebase_admin
|
| 16 |
import logging
|
| 17 |
import traceback
|
| 18 |
+
import unicodedata # For accent normalization
|
| 19 |
from bs4 import BeautifulSoup, Comment
|
| 20 |
|
| 21 |
try:
|
|
|
|
| 241 |
message = data.get('message')
|
| 242 |
|
| 243 |
if not feedback_type or not message:
|
| 244 |
+
return jsonify({'error': 'Feedback type and message are_required'}), 400
|
| 245 |
|
| 246 |
user_data = db.reference(f'users/{uid}').get()
|
| 247 |
user_email = user_data.get('email', 'unknown_email')
|
|
|
|
| 526 |
# NBA Analytics Hub Data Fetching Utilities
|
| 527 |
# ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 528 |
|
| 529 |
+
def normalize_string(s):
|
| 530 |
+
"""Removes accent marks and converts to lowercase for consistent comparison."""
|
| 531 |
+
if not isinstance(s, str):
|
| 532 |
+
return str(s)
|
| 533 |
+
s = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('utf-8')
|
| 534 |
+
return s.strip()
|
| 535 |
+
|
| 536 |
def clean_firebase_keys(key_name):
|
| 537 |
if not isinstance(key_name, str):
|
| 538 |
key_name = str(key_name)
|
|
|
|
| 658 |
return df
|
| 659 |
|
| 660 |
def _scrape_player_index_brscraper():
|
| 661 |
+
# Prioritize getting real player data from recent seasons
|
| 662 |
+
seasons_to_try_for_index = get_available_seasons_util(num_seasons=2) # Try current and previous season
|
| 663 |
+
|
| 664 |
+
for season_str in seasons_to_try_for_index:
|
| 665 |
+
end_year = int(season_str.split('β')[1])
|
| 666 |
+
try:
|
| 667 |
+
logging.info(f"Attempting to get player index for year: {end_year} from BRScraper...")
|
| 668 |
+
df = nba.get_stats(end_year, info='per_game', rename=False)
|
| 669 |
+
|
| 670 |
+
if not df.empty and 'Player' in df.columns:
|
| 671 |
+
player_names = df['Player'].dropna().unique().tolist()
|
| 672 |
+
# Normalize player names before returning
|
| 673 |
+
player_names = [normalize_string(name) for name in player_names]
|
| 674 |
+
logging.info(f"Successfully retrieved {len(player_names)} players for index from {season_str}.")
|
| 675 |
+
return pd.DataFrame({'name': player_names})
|
| 676 |
+
else:
|
| 677 |
+
logging.warning(f"Player index DataFrame empty or 'Player' column missing for {season_str}. Trying next season.")
|
| 678 |
+
except Exception as e:
|
| 679 |
+
logging.warning(f"Error fetching player index with BRScraper for {season_str}: {e}. Trying next season.")
|
| 680 |
+
|
| 681 |
+
# Fallback to a curated list if recent seasons fail
|
| 682 |
+
logging.error("Failed to fetch player index from recent seasons. Falling back to curated common players list.")
|
| 683 |
+
common_players = [
|
| 684 |
+
'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo',
|
| 685 |
+
'Nikola Jokic', # No accent here, as it will be normalized
|
| 686 |
+
'Joel Embiid', 'Jayson Tatum', 'Luka Doncic', # No accent here, as it will be normalized
|
| 687 |
+
'Damian Lillard', 'Jimmy Butler', 'Kawhi Leonard', 'Paul George',
|
| 688 |
+
'Anthony Davis', 'Rudy Gobert', 'Donovan Mitchell', 'Trae Young',
|
| 689 |
+
'Devin Booker', 'Karl-Anthony Towns', 'Zion Williamson', 'Ja Morant',
|
| 690 |
+
'Shai Gilgeous-Alexander', 'Tyrese Maxey', 'Anthony Edwards', 'Victor Wembanyama',
|
| 691 |
+
'Jalen Brunson', 'Paolo Banchero', 'Franz Wagner', 'Cade Cunningham'
|
| 692 |
+
]
|
| 693 |
+
return pd.DataFrame({'name': common_players})
|
| 694 |
|
| 695 |
def get_player_career_stats_brscraper(player_name, seasons_to_check=10, playoffs=False):
|
| 696 |
if not BRSCRAPER_AVAILABLE:
|
|
|
|
| 698 |
return pd.DataFrame()
|
| 699 |
all_rows = []
|
| 700 |
|
| 701 |
+
# Normalize the input player name for consistent lookup
|
| 702 |
+
normalized_player_name = normalize_string(player_name)
|
| 703 |
|
| 704 |
+
seasons_to_try = get_available_seasons_util(seasons_to_check)
|
| 705 |
|
| 706 |
for season_str in seasons_to_try:
|
| 707 |
end_year = int(season_str.split('β')[1])
|
| 708 |
+
|
| 709 |
+
# Implement retry logic for each season fetch
|
| 710 |
+
for attempt in range(3): # Try up to 3 times
|
| 711 |
+
try:
|
| 712 |
+
logging.info(f"DEBUG: Attempt {attempt+1} for nba.get_stats for player '{player_name}' in season {season_str} (year: {end_year}, playoffs: {playoffs})...")
|
| 713 |
+
|
| 714 |
+
df_season = nba.get_stats(end_year, info='per_game', playoffs=playoffs, rename=False)
|
| 715 |
+
|
| 716 |
+
if df_season.empty:
|
| 717 |
+
logging.warning(f"DEBUG: nba.get_stats returned empty DataFrame for {player_name} in {season_str} on attempt {attempt+1}. Retrying...")
|
| 718 |
+
time.sleep(1) # Wait a bit before retrying
|
| 719 |
+
continue # Go to next attempt
|
| 720 |
+
|
| 721 |
+
if 'Player' not in df_season.columns:
|
| 722 |
+
logging.warning(f"DEBUG: DataFrame for {player_name} in {season_str} has no 'Player' column on attempt {attempt+1}. Columns: {df_season.columns.tolist()}. Retrying...")
|
| 723 |
+
time.sleep(1)
|
| 724 |
+
continue
|
| 725 |
+
|
| 726 |
+
# Normalize player names in the DataFrame for comparison
|
| 727 |
+
df_season['Player_Normalized'] = df_season['Player'].apply(normalize_string)
|
| 728 |
+
|
| 729 |
+
row = df_season[df_season['Player_Normalized'] == normalized_player_name]
|
| 730 |
+
|
| 731 |
+
if not row.empty:
|
| 732 |
+
row = row.copy()
|
| 733 |
+
row['Season'] = season_str
|
| 734 |
+
# Remove the temporary normalized column before appending
|
| 735 |
+
row = row.drop(columns=['Player_Normalized'], errors='ignore')
|
| 736 |
+
all_rows.append(row)
|
| 737 |
+
logging.info(f"DEBUG: Found stats for {player_name} in {season_str} on attempt {attempt+1}. Appending row.")
|
| 738 |
+
break # Break retry loop if successful
|
| 739 |
+
else:
|
| 740 |
+
logging.info(f"DEBUG: Player {player_name} not found in {season_str} stats (after getting season data) on attempt {attempt+1}. Retrying...")
|
| 741 |
+
time.sleep(1)
|
| 742 |
+
continue # Go to next attempt
|
| 743 |
|
| 744 |
+
except Exception as e:
|
| 745 |
+
logging.warning(f"DEBUG: Exception on attempt {attempt+1} when fetching {season_str} {'playoff' if playoffs else 'regular season'} stats for {player_name}: {e}")
|
| 746 |
+
time.sleep(1) # Wait before next retry
|
| 747 |
+
if attempt == 2: # If last attempt failed
|
| 748 |
+
logging.error(f"DEBUG: All 3 attempts failed for {player_name} in {season_str}. Giving up on this season.")
|
| 749 |
+
continue # Go to next attempt
|
| 750 |
|
| 751 |
if not all_rows:
|
| 752 |
logging.warning(f"DEBUG: No stats found for {player_name} across all attempted seasons. Returning empty DataFrame.")
|
|
|
|
| 769 |
if col not in non_num:
|
| 770 |
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 771 |
|
| 772 |
+
df['Player'] = player_name # Ensure original player name is kept
|
| 773 |
df = df.replace({np.nan: None})
|
| 774 |
return df
|
| 775 |
|
|
|
|
| 947 |
all_player_season_data = []
|
| 948 |
players_with_no_data = []
|
| 949 |
|
|
|
|
| 950 |
for player_name in selected_players:
|
|
|
|
|
|
|
| 951 |
df_player_career = get_player_career_stats_brscraper(player_name, playoffs=False)
|
| 952 |
|
| 953 |
if df_player_career.empty:
|
| 954 |
logging.info(f"No career data found for {player_name}. Adding to no_data list.")
|
| 955 |
players_with_no_data.append(player_name)
|
| 956 |
+
continue
|
| 957 |
|
|
|
|
|
|
|
| 958 |
filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
|
| 959 |
|
| 960 |
if not filtered_df.empty:
|
|
|
|
| 1303 |
return jsonify({'error': 'Award type and criteria are required'}), 400
|
| 1304 |
|
| 1305 |
prompt = f"Predict top 5 {award_type} candidates based on {criteria}. Focus on 2024-25 season."
|
| 1306 |
+
prediction = ask_perp(prompt)
|
| 1307 |
if "Error from AI" in prediction:
|
| 1308 |
return jsonify({'error': prediction}), 500
|
| 1309 |
|
|
|
|
| 1336 |
"4. Comparison to similar players at the same age. 5. Career trajectory prediction. "
|
| 1337 |
"Base your analysis on historical player development patterns and current NBA trends."
|
| 1338 |
)
|
| 1339 |
+
projection = ask_perp(prompt)
|
| 1340 |
if "Error from AI" in projection:
|
| 1341 |
return jsonify({'error': projection}), 500
|
| 1342 |
|
|
|
|
| 1362 |
if "Error from AI" in similar_players_analysis:
|
| 1363 |
return jsonify({'error': similar_players_analysis}), 500
|
| 1364 |
|
|
|
|
| 1365 |
auth_header = request.headers.get('Authorization', '')
|
| 1366 |
token = auth_header.split(' ')[1]
|
| 1367 |
uid = verify_token(token)
|
| 1368 |
|
| 1369 |
+
analysis_id = str(uuid.uuid4())
|
| 1370 |
|
| 1371 |
if FIREBASE_INITIALIZED:
|
| 1372 |
user_analyses_ref = db.reference(f'user_analyses/{uid}')
|
|
|
|
| 1389 |
logging.error(f"Error in /api/nba/similar_players: {e}")
|
| 1390 |
return jsonify({'error': str(e)}), 500
|
| 1391 |
|
| 1392 |
+
@app.route('/api/nba/manual_player_compare', methods=['POST'])
|
| 1393 |
+
@credit_required(cost=1)
|
| 1394 |
+
@cross_origin()
|
| 1395 |
+
def manual_player_compare():
|
| 1396 |
+
try:
|
| 1397 |
+
data = request.get_json()
|
| 1398 |
+
player1_name = data.get('player1_name')
|
| 1399 |
+
player1_season = data.get('player1_season')
|
| 1400 |
+
player2_name = data.get('player2_name')
|
| 1401 |
+
player2_season = data.get('player2_season')
|
| 1402 |
+
|
| 1403 |
+
if not player1_name or not player2_name:
|
| 1404 |
+
return jsonify({'error': 'Both player names are required'}), 400
|
| 1405 |
+
|
| 1406 |
+
player1_str = f"{player1_name} ({player1_season} season)" if player1_season else player1_name
|
| 1407 |
+
player2_str = f"{player2_name} ({player2_season} season)" if player2_season else player2_name
|
| 1408 |
+
|
| 1409 |
+
comparison_context = "Statistical comparison"
|
| 1410 |
+
if player1_season and player2_season:
|
| 1411 |
+
comparison_context += f" (specifically {player1_season} vs {player2_season} seasons)"
|
| 1412 |
+
elif player1_season:
|
| 1413 |
+
comparison_context += f" (specifically {player1_season} season for {player1_name} vs {player2_name}'s career/prime)"
|
| 1414 |
+
elif player2_season:
|
| 1415 |
+
comparison_context += f" (specifically {player1_name}'s career/prime vs {player2_season} season for {player2_name})"
|
| 1416 |
+
else:
|
| 1417 |
+
comparison_context += " (career/prime comparison)"
|
| 1418 |
+
|
| 1419 |
+
prompt = (
|
| 1420 |
+
f"Compare {player1_str} vs {player2_str} in detail: "
|
| 1421 |
+
f"1. {comparison_context}. "
|
| 1422 |
+
"2. Playing style similarities and differences. 3. Strengths and weaknesses of each. "
|
| 1423 |
+
"4. Team impact and role. 5. Overall similarity score (1-10). "
|
| 1424 |
+
"Provide a comprehensive comparison with specific examples."
|
| 1425 |
+
)
|
| 1426 |
+
|
| 1427 |
+
comparison = ask_perp(prompt)
|
| 1428 |
+
if "Error from AI" in comparison:
|
| 1429 |
+
return jsonify({'error': comparison}), 500
|
| 1430 |
+
|
| 1431 |
+
return jsonify({'comparison': comparison})
|
| 1432 |
+
except Exception as e:
|
| 1433 |
+
logging.error(f"Error in /api/nba/manual_player_compare: {e}")
|
| 1434 |
+
return jsonify({'error': str(e)}), 500
|
| 1435 |
+
|
| 1436 |
|
| 1437 |
if __name__ == '__main__':
|
| 1438 |
app.run(debug=True, host="0.0.0.0", port=7860)
|