2735 lines
105 KiB
Python
2735 lines
105 KiB
Python
from flask import Flask, render_template, request, redirect, jsonify, session
|
||
import json
|
||
import os
|
||
import random
|
||
import glob
|
||
from collections import defaultdict
|
||
from datetime import datetime
|
||
import re
|
||
|
||
app = Flask(__name__)
|
||
app.secret_key = 'your-secret-key-for-sessions' # Change this to a random secret key
|
||
|
||
# Language support
|
||
SUPPORTED_LANGUAGES = ['sl', 'en']
|
||
DEFAULT_LANGUAGE = 'sl'
|
||
|
||
def load_translations(language='sl'):
|
||
"""Load translations for the specified language"""
|
||
try:
|
||
locale_file = f'locales/{language}.json'
|
||
if os.path.exists(locale_file):
|
||
with open(locale_file, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
print(f"Error loading translations for {language}: {e}")
|
||
|
||
# Fallback to default language
|
||
try:
|
||
with open(f'locales/{DEFAULT_LANGUAGE}.json', 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
print(f"Error loading default translations: {e}")
|
||
return {}
|
||
|
||
def get_current_language():
|
||
"""Get current language from session or default"""
|
||
return session.get('language', DEFAULT_LANGUAGE)
|
||
|
||
def get_translations():
|
||
"""Get translations for current language"""
|
||
return load_translations(get_current_language())
|
||
|
||
def is_mobile_device():
|
||
"""Check if the request is coming from a mobile device"""
|
||
user_agent = request.headers.get('User-Agent', '').lower()
|
||
mobile_patterns = [
|
||
r'android', r'iphone', r'ipad', r'ipod', r'blackberry',
|
||
r'iemobile', r'opera mini', r'mobile', r'tablet'
|
||
]
|
||
return any(re.search(pattern, user_agent) for pattern in mobile_patterns)
|
||
|
||
def calculate_tens_from_targets(targets):
|
||
"""Calculate the number of 10s from a targets dictionary"""
|
||
tens_count = 0
|
||
if not targets:
|
||
return 0
|
||
|
||
for target in targets.values():
|
||
if isinstance(target, dict):
|
||
for shot_key, shot_value in target.items():
|
||
if shot_key.startswith('shot') and shot_value == 10:
|
||
tens_count += 1
|
||
|
||
return tens_count
|
||
|
||
# Define streams globally so both routes can access them
|
||
STREAMS = [
|
||
{'name': 'Target1', 'url': 'http://192.168.0.134:9081'},
|
||
{'name': 'Target2', 'url': 'http://192.168.0.134:9082'},
|
||
{'name': 'Target3', 'url': 'http://192.168.0.134:9083'},
|
||
{'name': 'Target4', 'url': 'http://192.168.0.134:9084'},
|
||
{'name': 'Target5', 'url': 'http://192.168.0.134:9085'},
|
||
{'name': 'Target6', 'url': 'http://192.168.0.134:9086'},
|
||
]
|
||
|
||
# Settings file paths
|
||
SETTINGS_FILE = 'camera_settings.json'
|
||
PLAYERS_FILE = 'players.json'
|
||
TOURNAMENT_FILE = 'tournament_state.json'
|
||
RESULTS_FILE = 'tournament_results.json'
|
||
LEAGUE_FILE = 'league_state.json'
|
||
ARCHIVE_DIR = 'tournament_archives'
|
||
LEAGUE_ARCHIVE_DIR = 'league_archives'
|
||
|
||
# Default settings
|
||
DEFAULT_SETTINGS = {
|
||
'camera_titles': {
|
||
'1': 'Camera 1',
|
||
'2': 'Camera 2',
|
||
'3': 'Camera 3',
|
||
'4': 'Camera 4',
|
||
'5': 'Camera 5',
|
||
'6': 'Camera 6'
|
||
},
|
||
'display_options': {
|
||
'show_titles': True,
|
||
'title_size': 1.1
|
||
}
|
||
}
|
||
|
||
# Default players structure
|
||
DEFAULT_PLAYERS = {
|
||
'players': [
|
||
{'id': 1, 'name': 'Player 1', 'enabled': True},
|
||
{'id': 2, 'name': 'Player 2', 'enabled': True},
|
||
{'id': 3, 'name': 'Player 3', 'enabled': True},
|
||
{'id': 4, 'name': 'Player 4', 'enabled': True},
|
||
{'id': 5, 'name': 'Player 5', 'enabled': True},
|
||
{'id': 6, 'name': 'Player 6', 'enabled': True},
|
||
]
|
||
}
|
||
|
||
def load_settings():
|
||
"""Load settings from JSON file, create with defaults if not exists"""
|
||
try:
|
||
if os.path.exists(SETTINGS_FILE):
|
||
with open(SETTINGS_FILE, 'r') as f:
|
||
settings = json.load(f)
|
||
# Ensure all required keys exist (for backwards compatibility)
|
||
for key in DEFAULT_SETTINGS:
|
||
if key not in settings:
|
||
settings[key] = DEFAULT_SETTINGS[key]
|
||
for camera_id in DEFAULT_SETTINGS['camera_titles']:
|
||
if camera_id not in settings['camera_titles']:
|
||
settings['camera_titles'][camera_id] = DEFAULT_SETTINGS['camera_titles'][camera_id]
|
||
return settings
|
||
else:
|
||
return DEFAULT_SETTINGS.copy()
|
||
except (json.JSONDecodeError, IOError):
|
||
return DEFAULT_SETTINGS.copy()
|
||
|
||
def save_settings(settings):
|
||
"""Save settings to JSON file"""
|
||
try:
|
||
with open(SETTINGS_FILE, 'w') as f:
|
||
json.dump(settings, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_players():
|
||
"""Load players from JSON file, create with defaults if not exists"""
|
||
try:
|
||
if os.path.exists(PLAYERS_FILE):
|
||
with open(PLAYERS_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
save_players(DEFAULT_PLAYERS)
|
||
return DEFAULT_PLAYERS.copy()
|
||
except (json.JSONDecodeError, IOError):
|
||
return DEFAULT_PLAYERS.copy()
|
||
|
||
def save_players(players_data):
|
||
"""Save players to JSON file"""
|
||
try:
|
||
with open(PLAYERS_FILE, 'w') as f:
|
||
json.dump(players_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_tournament_state():
|
||
"""Load tournament state from JSON file"""
|
||
try:
|
||
if os.path.exists(TOURNAMENT_FILE):
|
||
with open(TOURNAMENT_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
return None
|
||
except (json.JSONDecodeError, IOError):
|
||
return None
|
||
|
||
def save_tournament_state(tournament_data):
|
||
"""Save tournament state to JSON file"""
|
||
try:
|
||
with open(TOURNAMENT_FILE, 'w') as f:
|
||
json.dump(tournament_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_league_state():
|
||
"""Load league state from JSON file"""
|
||
try:
|
||
if os.path.exists(LEAGUE_FILE):
|
||
with open(LEAGUE_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
return None
|
||
except (json.JSONDecodeError, IOError):
|
||
return None
|
||
|
||
def save_league_state(league_data):
|
||
"""Save league state to JSON file"""
|
||
try:
|
||
with open(LEAGUE_FILE, 'w') as f:
|
||
json.dump(league_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_results():
|
||
"""Load results from JSON file"""
|
||
try:
|
||
if os.path.exists(RESULTS_FILE):
|
||
with open(RESULTS_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
return None
|
||
except (json.JSONDecodeError, IOError):
|
||
return None
|
||
|
||
def save_results(results_data):
|
||
"""Save results to JSON file"""
|
||
try:
|
||
with open(RESULTS_FILE, 'w') as f:
|
||
json.dump(results_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def archive_tournament(tournament_data, results_data):
|
||
"""Archive completed tournament data"""
|
||
try:
|
||
# Create archive directory if it doesn't exist
|
||
if not os.path.exists(ARCHIVE_DIR):
|
||
os.makedirs(ARCHIVE_DIR)
|
||
|
||
# Create filename with timestamp
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
archive_filename = f"tournament_{timestamp}.json"
|
||
archive_path = os.path.join(ARCHIVE_DIR, archive_filename)
|
||
|
||
# Combine tournament and results data
|
||
archive_data = {
|
||
'tournament': tournament_data,
|
||
'results': results_data,
|
||
'archived_at': datetime.now().isoformat()
|
||
}
|
||
|
||
# Save to archive
|
||
with open(archive_path, 'w') as f:
|
||
json.dump(archive_data, f, indent=2)
|
||
|
||
print(f"Tournament archived to: {archive_path}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"Error archiving tournament: {e}")
|
||
return False
|
||
|
||
def archive_league(league_data):
|
||
"""Archive completed league data"""
|
||
try:
|
||
if not os.path.exists(LEAGUE_ARCHIVE_DIR):
|
||
os.makedirs(LEAGUE_ARCHIVE_DIR)
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
archive_filename = f"league_{timestamp}.json"
|
||
archive_path = os.path.join(LEAGUE_ARCHIVE_DIR, archive_filename)
|
||
|
||
archive_data = {
|
||
'league': league_data,
|
||
'archived_at': datetime.now().isoformat()
|
||
}
|
||
|
||
with open(archive_path, 'w') as f:
|
||
json.dump(archive_data, f, indent=2)
|
||
|
||
print(f"League archived to: {archive_path}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"Error archiving league: {e}")
|
||
return False
|
||
|
||
def create_league(enabled_players, tournament_type):
|
||
"""Create a new league with 5 tournaments (changed from 6)"""
|
||
league_id = f"league_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||
|
||
league_data = {
|
||
'league_id': league_id,
|
||
'created_at': datetime.now().isoformat(),
|
||
'tournament_type': tournament_type,
|
||
'total_tournaments': 5, # Changed from 6 to 5
|
||
'current_tournament': 0, # Will be 1 when first tournament starts
|
||
'participants': {},
|
||
'completed_tournaments': [],
|
||
'league_finished': False
|
||
}
|
||
|
||
# Initialize participants
|
||
for player in enabled_players:
|
||
league_data['participants'][str(player['id'])] = {
|
||
'name': player['name'],
|
||
'joker_used': False,
|
||
'tournament_results': [],
|
||
'total_score': 0,
|
||
'final_score': 0, # Best 4 tournaments (changed from 5)
|
||
'tournaments_participated': 0
|
||
}
|
||
|
||
return league_data
|
||
|
||
def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None):
|
||
"""Create draft groups of 6 players and organize rounds"""
|
||
if len(enabled_players) < 1:
|
||
return None
|
||
|
||
# Shuffle players for random grouping
|
||
players_copy = enabled_players.copy()
|
||
random.shuffle(players_copy)
|
||
|
||
# Create groups of up to 6
|
||
groups = []
|
||
for i in range(0, len(players_copy), 6):
|
||
group = players_copy[i:i+6]
|
||
groups.append(group)
|
||
|
||
# Create rounds
|
||
rounds = []
|
||
for i, group in enumerate(groups):
|
||
rounds.append({
|
||
'round_number': i + 1,
|
||
'players': group,
|
||
'status': 'pending' if i == 0 else 'waiting'
|
||
})
|
||
|
||
tournament_data = {
|
||
'rounds': rounds,
|
||
'created_at': datetime.now().isoformat(),
|
||
'total_players': len(enabled_players),
|
||
'total_rounds': len(rounds),
|
||
'current_round': 1,
|
||
'tournament_type': tournament_type
|
||
}
|
||
|
||
if league_tournament_number:
|
||
tournament_data['league_tournament_number'] = league_tournament_number
|
||
|
||
return tournament_data
|
||
|
||
def create_results_structure(tournament_data):
|
||
"""Create results structure for a tournament"""
|
||
if not tournament_data:
|
||
return None
|
||
|
||
tournament_type = tournament_data.get('tournament_type', '20_targets')
|
||
|
||
# Determine target count and shots per target
|
||
if tournament_type == '40_targets':
|
||
num_targets = 40
|
||
shots_per_target = 2
|
||
elif tournament_type == '4_targets':
|
||
num_targets = 4
|
||
shots_per_target = 5
|
||
else: # 20_targets (default)
|
||
num_targets = 20
|
||
shots_per_target = 2
|
||
|
||
results = {
|
||
'tournament_id': tournament_data.get('created_at', datetime.now().isoformat()),
|
||
'tournament_type': tournament_type,
|
||
'participants': {},
|
||
'tournament_finished': False,
|
||
'created_at': datetime.now().isoformat()
|
||
}
|
||
|
||
# Add league info if present
|
||
if 'league_tournament_number' in tournament_data:
|
||
results['league_tournament_number'] = tournament_data['league_tournament_number']
|
||
|
||
# Create structure for each participant
|
||
all_players = []
|
||
for round_data in tournament_data['rounds']:
|
||
all_players.extend(round_data['players'])
|
||
|
||
for player in all_players:
|
||
player_id = str(player['id'])
|
||
|
||
# Create target structure based on tournament type
|
||
targets = {}
|
||
for i in range(1, num_targets + 1):
|
||
target = {}
|
||
for j in range(1, shots_per_target + 1):
|
||
target[f'shot{j}'] = None
|
||
targets[str(i)] = target
|
||
|
||
results['participants'][player_id] = {
|
||
'name': player['name'],
|
||
'targets': targets,
|
||
'total_score': 0,
|
||
'completed': False
|
||
}
|
||
|
||
return results
|
||
|
||
def calculate_total_score(targets):
|
||
"""Calculate total score from targets, treating None as 0"""
|
||
total = 0
|
||
for target in targets.values():
|
||
for shot_key, shot_value in target.items():
|
||
if shot_key.startswith('shot') and shot_value is not None:
|
||
total += shot_value
|
||
return total
|
||
|
||
def is_participant_completed(targets):
|
||
"""Check if a participant has completed all targets (all shots entered, including 0s)"""
|
||
for target in targets.values():
|
||
for shot_key, shot_value in target.items():
|
||
if shot_key.startswith('shot') and shot_value is None:
|
||
return False
|
||
return True
|
||
|
||
def calculate_league_final_scores(league_data):
|
||
"""Calculate final league scores using best 4 tournaments (changed from best 5)"""
|
||
for participant_id, participant in league_data['participants'].items():
|
||
tournament_scores = []
|
||
|
||
# Get all tournament scores where player participated
|
||
for result in participant['tournament_results']:
|
||
if result['participated']:
|
||
tournament_scores.append(result['score'])
|
||
|
||
# Sort scores descending and take best 4 (changed from 5)
|
||
tournament_scores.sort(reverse=True)
|
||
best_scores = tournament_scores[:4] if len(tournament_scores) > 4 else tournament_scores
|
||
|
||
participant['final_score'] = sum(best_scores)
|
||
participant['tournaments_participated'] = len(tournament_scores)
|
||
|
||
def get_league_final_rankings(league_data):
|
||
"""Get final league rankings sorted by final score"""
|
||
participants = []
|
||
for player_id, data in league_data['participants'].items():
|
||
# Calculate total 10s across all tournaments
|
||
total_tens = sum(result.get('tens_count', 0) for result in data['tournament_results'] if result.get('participated', False))
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'final_score': data['final_score'],
|
||
'total_score': data['total_score'],
|
||
'tournaments_participated': data['tournaments_participated'],
|
||
'joker_used': data['joker_used'],
|
||
'tournament_results': data['tournament_results'],
|
||
'total_tens': total_tens
|
||
})
|
||
|
||
# Sort by final score (best 5 tournaments) descending, then by total 10s for tiebreaking
|
||
participants.sort(key=lambda x: (x['final_score'], x['total_tens']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return participants
|
||
|
||
def get_current_round_data():
|
||
"""Get current round data from tournament"""
|
||
tournament_state = load_tournament_state()
|
||
if not tournament_state:
|
||
return None
|
||
|
||
current_round_num = tournament_state.get('current_round', 1)
|
||
|
||
# Find the current round
|
||
for round_data in tournament_state['rounds']:
|
||
if round_data['round_number'] == current_round_num:
|
||
return round_data
|
||
|
||
return None
|
||
|
||
def calculate_current_league_standings(league_data):
|
||
"""Calculate current league standings during active league"""
|
||
participants = []
|
||
for player_id, data in league_data['participants'].items():
|
||
# Calculate current standings based on completed tournaments
|
||
tournament_scores = []
|
||
completed_tournaments = 0
|
||
total_tens = 0
|
||
|
||
for result in data['tournament_results']:
|
||
if result['participated']:
|
||
tournament_scores.append(result['score'])
|
||
completed_tournaments += 1
|
||
total_tens += result.get('tens_count', 0)
|
||
|
||
# Current score is sum of all completed tournaments
|
||
current_total = sum(tournament_scores)
|
||
|
||
# For display, show what the final score would be if we took best 5 now
|
||
tournament_scores.sort(reverse=True)
|
||
projected_final = sum(tournament_scores[:5]) if len(tournament_scores) >= 5 else sum(tournament_scores)
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'current_total': current_total,
|
||
'projected_final': projected_final,
|
||
'tournaments_completed': completed_tournaments,
|
||
'joker_used': data['joker_used'],
|
||
'tournament_results': data['tournament_results'],
|
||
'total_tens': total_tens
|
||
})
|
||
|
||
# Sort by current total score (descending), then by total 10s for tiebreaking
|
||
participants.sort(key=lambda x: (x['current_total'], x['total_tens']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return participants
|
||
|
||
def get_league_current_standings(league_state):
|
||
"""Get current league standings including in-progress tournament"""
|
||
if not league_state:
|
||
return []
|
||
|
||
# Start with current league participants
|
||
participants = []
|
||
for player_id, data in league_state['participants'].items():
|
||
# Calculate total 10s from completed tournaments
|
||
total_tens = sum(result.get('tens_count', 0) for result in data['tournament_results'] if result.get('participated', False))
|
||
|
||
participant = {
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'total_score': data['total_score'],
|
||
'final_score': data['final_score'],
|
||
'tournaments_participated': data['tournaments_participated'],
|
||
'joker_used': data['joker_used'],
|
||
'tournament_results': data['tournament_results'],
|
||
'total_tens': total_tens,
|
||
'current_tournament_score': 0, # Score from current tournament
|
||
'current_tournament_participating': False
|
||
}
|
||
participants.append(participant)
|
||
|
||
# Add current tournament scores if available
|
||
current_results = load_results()
|
||
if current_results and not current_results.get('tournament_finished', False):
|
||
for player_id, result_data in current_results['participants'].items():
|
||
# Find this participant in our list
|
||
for participant in participants:
|
||
if participant['id'] == player_id:
|
||
participant['current_tournament_score'] = result_data['total_score']
|
||
participant['current_tournament_participating'] = True
|
||
break
|
||
|
||
# Sort by final score (best 5 tournaments) descending, then by total 10s for tiebreaking
|
||
participants.sort(key=lambda x: (x['final_score'], x['total_tens']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return participants
|
||
|
||
# Add these functions after the existing helper functions in app.py
|
||
|
||
def get_archived_tournaments():
|
||
"""Get list of standalone archived tournaments (excluding league tournaments)"""
|
||
try:
|
||
if not os.path.exists(ARCHIVE_DIR):
|
||
return []
|
||
|
||
archives = []
|
||
for file_path in glob.glob(os.path.join(ARCHIVE_DIR, "tournament_*.json")):
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
# Extract metadata
|
||
filename = os.path.basename(file_path)
|
||
archived_at = data.get('archived_at', 'Unknown')
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
# Skip tournaments that are part of a league
|
||
if tournament_data.get('league_tournament_number') or results_data.get('league_tournament_number'):
|
||
continue
|
||
|
||
archive_info = {
|
||
'filename': filename,
|
||
'filepath': file_path,
|
||
'archived_at': archived_at,
|
||
'created_at': tournament_data.get('created_at', 'Unknown'),
|
||
'tournament_type': tournament_data.get('tournament_type', '20_targets'),
|
||
'total_players': tournament_data.get('total_players', 0),
|
||
'total_rounds': tournament_data.get('total_rounds', 0),
|
||
'tournament_finished': results_data.get('tournament_finished', False),
|
||
'participants_count': len(results_data.get('participants', {}))
|
||
}
|
||
|
||
archives.append(archive_info)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"Error reading archive {file_path}: {e}")
|
||
continue
|
||
|
||
# Sort by archived date (newest first)
|
||
archives.sort(key=lambda x: x['archived_at'], reverse=True)
|
||
return archives
|
||
except Exception as e:
|
||
print(f"Error getting archived tournaments: {e}")
|
||
return []
|
||
|
||
def get_archived_leagues():
|
||
"""Get list of all archived leagues"""
|
||
try:
|
||
if not os.path.exists(LEAGUE_ARCHIVE_DIR):
|
||
return []
|
||
|
||
archives = []
|
||
for file_path in glob.glob(os.path.join(LEAGUE_ARCHIVE_DIR, "league_*.json")):
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
filename = os.path.basename(file_path)
|
||
archived_at = data.get('archived_at', 'Unknown')
|
||
league_data = data.get('league', {})
|
||
|
||
archive_info = {
|
||
'filename': filename,
|
||
'filepath': file_path,
|
||
'archived_at': archived_at,
|
||
'created_at': league_data.get('created_at', 'Unknown'),
|
||
'league_id': league_data.get('league_id', 'Unknown'),
|
||
'tournament_type': league_data.get('tournament_type', '20_targets'),
|
||
'total_tournaments': league_data.get('total_tournaments', 6),
|
||
'participants_count': len(league_data.get('participants', {})),
|
||
'league_finished': league_data.get('league_finished', False),
|
||
'completed_tournaments': len(league_data.get('completed_tournaments', []))
|
||
}
|
||
|
||
archives.append(archive_info)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"Error reading league archive {file_path}: {e}")
|
||
continue
|
||
|
||
# Sort by archived date (newest first)
|
||
archives.sort(key=lambda x: x['archived_at'], reverse=True)
|
||
return archives
|
||
except Exception as e:
|
||
print(f"Error getting archived leagues: {e}")
|
||
return []
|
||
|
||
def load_archive_file(filepath):
|
||
"""Load a specific archive file"""
|
||
try:
|
||
with open(filepath, 'r') as f:
|
||
return json.load(f)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"Error loading archive file {filepath}: {e}")
|
||
return None
|
||
|
||
def analyze_player_performance(player_id, archives_data):
|
||
"""Analyze performance of a specific player across all archives"""
|
||
player_stats = {
|
||
'total_tournaments': 0,
|
||
'total_leagues': 0,
|
||
'tournament_scores': [],
|
||
'league_scores': [],
|
||
'best_tournament_score': 0,
|
||
'worst_tournament_score': float('inf'),
|
||
'average_tournament_score': 0,
|
||
'total_shots_fired': 0,
|
||
'performance_trend': [],
|
||
'tournament_history': [],
|
||
'league_history': []
|
||
}
|
||
|
||
# Process tournament archives
|
||
for archive in archives_data.get('tournaments', []):
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if not data:
|
||
continue
|
||
|
||
results = data.get('results', {})
|
||
participants = results.get('participants', {})
|
||
|
||
if str(player_id) in participants:
|
||
participant = participants[str(player_id)]
|
||
score = participant.get('total_score', 0)
|
||
completed = participant.get('completed', False)
|
||
|
||
if completed:
|
||
player_stats['total_tournaments'] += 1
|
||
player_stats['tournament_scores'].append(score)
|
||
|
||
if score > player_stats['best_tournament_score']:
|
||
player_stats['best_tournament_score'] = score
|
||
if score < player_stats['worst_tournament_score']:
|
||
player_stats['worst_tournament_score'] = score
|
||
|
||
# Count shots fired - NOW WITH PROPER COUNTING FOR ALL FORMATS
|
||
tournament_type = archive.get('tournament_type', '20_targets')
|
||
shots_in_tournament = count_shots_in_tournament(participant, tournament_type)
|
||
|
||
player_stats['total_shots_fired'] += shots_in_tournament
|
||
|
||
# Add to history
|
||
player_stats['tournament_history'].append({
|
||
'date': archive['archived_at'],
|
||
'score': score,
|
||
'tournament_type': archive['tournament_type'],
|
||
'completed': completed,
|
||
'shots_fired': shots_in_tournament # NOW CORRECTLY CALCULATED
|
||
})
|
||
except Exception as e:
|
||
print(f"Error analyzing tournament archive: {e}")
|
||
continue
|
||
|
||
# Process league archives
|
||
for archive in archives_data.get('leagues', []):
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if not data:
|
||
continue
|
||
|
||
league = data.get('league', {})
|
||
participants = league.get('participants', {})
|
||
|
||
if str(player_id) in participants:
|
||
participant = participants[str(player_id)]
|
||
final_score = participant.get('final_score', 0)
|
||
total_score = participant.get('total_score', 0)
|
||
tournaments_participated = participant.get('tournaments_participated', 0)
|
||
|
||
player_stats['total_leagues'] += 1
|
||
player_stats['league_scores'].append(final_score)
|
||
|
||
player_stats['league_history'].append({
|
||
'date': archive['archived_at'],
|
||
'final_score': final_score,
|
||
'total_score': total_score,
|
||
'tournaments_participated': tournaments_participated,
|
||
'joker_used': participant.get('joker_used', False),
|
||
'tournament_results': participant.get('tournament_results', [])
|
||
})
|
||
except Exception as e:
|
||
print(f"Error analyzing league archive: {e}")
|
||
continue
|
||
|
||
# Calculate averages and trends
|
||
if player_stats['tournament_scores']:
|
||
player_stats['average_tournament_score'] = sum(player_stats['tournament_scores']) / len(player_stats['tournament_scores'])
|
||
|
||
# Performance trend (simple moving average)
|
||
if len(player_stats['tournament_scores']) >= 3:
|
||
for i in range(2, len(player_stats['tournament_scores'])):
|
||
avg = sum(player_stats['tournament_scores'][i-2:i+1]) / 3
|
||
player_stats['performance_trend'].append(avg)
|
||
|
||
if player_stats['worst_tournament_score'] == float('inf'):
|
||
player_stats['worst_tournament_score'] = 0
|
||
|
||
# Sort histories by date (newest first)
|
||
player_stats['tournament_history'].sort(key=lambda x: x['date'], reverse=True)
|
||
player_stats['league_history'].sort(key=lambda x: x['date'], reverse=True)
|
||
|
||
return player_stats
|
||
|
||
def get_all_players_from_archives():
|
||
"""Get all players that appear in any archive"""
|
||
all_players = {}
|
||
|
||
# Get current players first
|
||
current_players = load_players()
|
||
for player in current_players['players']:
|
||
all_players[str(player['id'])] = {
|
||
'id': player['id'],
|
||
'name': player['name'],
|
||
'current_player': True
|
||
}
|
||
|
||
# Add players from tournament archives
|
||
tournaments = get_archived_tournaments()
|
||
for archive in tournaments:
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if data and 'results' in data:
|
||
participants = data['results'].get('participants', {})
|
||
for player_id, participant in participants.items():
|
||
if player_id not in all_players:
|
||
all_players[player_id] = {
|
||
'id': int(player_id),
|
||
'name': participant.get('name', f'Player {player_id}'),
|
||
'current_player': False
|
||
}
|
||
except Exception as e:
|
||
print(f"Error processing tournament archive: {e}")
|
||
continue
|
||
|
||
# Add players from league archives
|
||
leagues = get_archived_leagues()
|
||
for archive in leagues:
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if data and 'league' in data:
|
||
participants = data['league'].get('participants', {})
|
||
for player_id, participant in participants.items():
|
||
if player_id not in all_players:
|
||
all_players[player_id] = {
|
||
'id': int(player_id),
|
||
'name': participant.get('name', f'Player {player_id}'),
|
||
'current_player': False
|
||
}
|
||
except Exception as e:
|
||
print(f"Error processing league archive: {e}")
|
||
continue
|
||
|
||
# Convert to list and sort by name
|
||
players_list = list(all_players.values())
|
||
players_list.sort(key=lambda x: x['name'])
|
||
|
||
return players_list
|
||
|
||
|
||
|
||
# Add these routes after the existing routes in app.py
|
||
|
||
# Add this to your app.py file to integrate the modern archive system
|
||
|
||
# Replace your existing archive routes with these updated ones:
|
||
|
||
@app.route('/archive')
|
||
def archive_index():
|
||
"""Archive index page with updated styling"""
|
||
if is_mobile_device():
|
||
return redirect('/mobile/archive')
|
||
|
||
tournaments = get_archived_tournaments() # Now only returns standalone tournaments
|
||
leagues = get_archived_leagues()
|
||
|
||
# Calculate overview stats
|
||
total_tournaments = len(tournaments)
|
||
total_leagues = len(leagues)
|
||
|
||
# Get total players from current players list
|
||
players_data = load_players()
|
||
total_players = len([p for p in players_data['players'] if p['enabled']])
|
||
|
||
# Calculate total competitions (standalone tournaments + completed leagues)
|
||
total_matches = total_tournaments + total_leagues
|
||
|
||
stats = {
|
||
'total_tournaments': total_tournaments,
|
||
'total_leagues': total_leagues,
|
||
'total_players': total_players,
|
||
'total_matches': total_matches
|
||
}
|
||
|
||
# Use the new template
|
||
return render_template('modern_archive_index.html',
|
||
tournaments=tournaments,
|
||
leagues=leagues,
|
||
stats=stats,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
@app.route('/archive/player-analysis')
|
||
def player_analysis():
|
||
"""Modern Player analysis page"""
|
||
if is_mobile_device():
|
||
return redirect('/mobile/archive/player-analysis')
|
||
|
||
all_players = get_all_players_from_archives()
|
||
|
||
# Calculate overview stats
|
||
total_players = len(all_players)
|
||
active_players = len([p for p in all_players if p['current_player']])
|
||
|
||
# Get average tournaments and top score from archives
|
||
avg_tournaments = 0
|
||
top_score = 0
|
||
|
||
if all_players:
|
||
total_tournament_count = 0
|
||
max_score = 0
|
||
|
||
for player in all_players:
|
||
# Get basic stats for each player
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
player_stats = analyze_player_performance(player['id'], archives_data)
|
||
total_tournament_count += player_stats['total_tournaments']
|
||
if player_stats['best_tournament_score'] > max_score:
|
||
max_score = player_stats['best_tournament_score']
|
||
|
||
avg_tournaments = total_tournament_count // total_players if total_players else 0
|
||
top_score = max_score
|
||
|
||
overview_stats = {
|
||
'total_players': total_players,
|
||
'active_players': active_players,
|
||
'avg_tournaments': avg_tournaments,
|
||
'top_score': top_score
|
||
}
|
||
|
||
return render_template('modern_player_analysis.html',
|
||
players=all_players,
|
||
overview_stats=overview_stats,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
@app.route('/archive/player/<int:player_id>')
|
||
def view_player_stats(player_id):
|
||
"""Modern Player stats page"""
|
||
if is_mobile_device():
|
||
return redirect(f'/mobile/archive/player/{player_id}')
|
||
|
||
all_players = get_all_players_from_archives()
|
||
player_info = next((p for p in all_players if p['id'] == player_id), None)
|
||
|
||
if not player_info:
|
||
return redirect('/archive/player-analysis')
|
||
|
||
# Get archives data
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
|
||
# Analyze player performance
|
||
player_stats = analyze_player_performance(player_id, archives_data)
|
||
|
||
return render_template('modern_player_stats.html',
|
||
player=player_info,
|
||
stats=player_stats,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
# Enhanced API endpoints for the modern archive system
|
||
|
||
@app.route('/api/archive/stats', methods=['GET'])
|
||
def api_get_archive_stats():
|
||
"""API endpoint to get archive overview statistics"""
|
||
try:
|
||
tournaments = get_archived_tournaments()
|
||
leagues = get_archived_leagues()
|
||
players_data = load_players()
|
||
|
||
# Calculate activity over time (last 6 months)
|
||
activity_data = {
|
||
'labels': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
||
'tournaments': [2, 4, 3, 5, 6, 4], # This should be calculated from actual data
|
||
'leagues': [1, 1, 2, 1, 2, 1]
|
||
}
|
||
|
||
# Calculate tournament type distribution - now includes 4_targets
|
||
type_distribution = {'20_targets': 0, '40_targets': 0, '4_targets': 0}
|
||
for tournament in tournaments:
|
||
tournament_type = tournament.get('tournament_type', '20_targets')
|
||
if tournament_type in type_distribution:
|
||
type_distribution[tournament_type] += 1
|
||
|
||
for league in leagues:
|
||
league_type = league.get('tournament_type', '20_targets')
|
||
if league_type in type_distribution:
|
||
type_distribution[league_type] += 1
|
||
|
||
stats = {
|
||
'overview': {
|
||
'total_tournaments': len(tournaments),
|
||
'total_leagues': len(leagues),
|
||
'total_players': len([p for p in players_data['players'] if p['enabled']]),
|
||
'total_matches': len(tournaments) + sum(l.get('completed_tournaments', 0) for l in leagues)
|
||
},
|
||
'activity_data': activity_data,
|
||
'type_distribution': {
|
||
'labels': ['20 Targets', '40 Targets', '4 Targets'],
|
||
'data': [type_distribution['20_targets'], type_distribution['40_targets'], type_distribution['4_targets']]
|
||
}
|
||
}
|
||
|
||
return jsonify({'status': 'success', 'stats': stats})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||
|
||
@app.route('/api/archive/players/with-stats', methods=['GET'])
|
||
def api_get_players_with_stats():
|
||
"""API endpoint to get all players with their basic stats"""
|
||
try:
|
||
all_players = get_all_players_from_archives()
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
|
||
players_with_stats = []
|
||
for player in all_players:
|
||
player_stats = analyze_player_performance(player['id'], archives_data)
|
||
|
||
players_with_stats.append({
|
||
'id': player['id'],
|
||
'name': player['name'],
|
||
'current_player': player['current_player'],
|
||
'stats': {
|
||
'total_tournaments': player_stats['total_tournaments'],
|
||
'total_leagues': player_stats['total_leagues'],
|
||
'best_tournament_score': player_stats['best_tournament_score'],
|
||
'average_tournament_score': player_stats['average_tournament_score'],
|
||
'total_shots_fired': player_stats['total_shots_fired'],
|
||
'performance_trend': player_stats['tournament_scores'][-8:] if len(player_stats['tournament_scores']) >= 8 else player_stats['tournament_scores'] # Last 8 tournaments for mini chart
|
||
}
|
||
})
|
||
|
||
return jsonify({'status': 'success', 'players': players_with_stats})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||
|
||
# Add these routes to handle clicking on archived tournaments/leagues
|
||
# These will load the archive data and show it in your existing result page format
|
||
|
||
@app.route('/archive/tournament/<filename>')
|
||
def view_archived_tournament(filename):
|
||
"""View archived tournament in results format"""
|
||
if is_mobile_device():
|
||
return redirect(f'/mobile/archive/tournament/{filename}')
|
||
|
||
filepath = os.path.join(ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/archive')
|
||
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
# Process results for display
|
||
participants = []
|
||
for player_id, participant_data in results_data.get('participants', {}).items():
|
||
targets = participant_data.get('targets', {})
|
||
tens_count = calculate_tens_from_targets(targets)
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': participant_data['name'],
|
||
'total_score': participant_data['total_score'],
|
||
'completed': participant_data['completed'],
|
||
'targets': targets,
|
||
'tens_count': tens_count
|
||
})
|
||
|
||
# Sort by score (descending), then by tens (descending) as tiebreaker
|
||
participants.sort(key=lambda x: (x['total_score'], x['tens_count']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
# Use the existing results display template but with archived data
|
||
return render_template('results_display.html',
|
||
results=results_data,
|
||
participants=participants,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at'),
|
||
'tournament_type': tournament_data.get('tournament_type', '20_targets')
|
||
})
|
||
|
||
@app.route('/archive/league/<filename>')
|
||
def view_archived_league(filename):
|
||
"""View archived league in results format"""
|
||
if is_mobile_device():
|
||
return redirect(f'/mobile/archive/league/{filename}')
|
||
|
||
filepath = os.path.join(LEAGUE_ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/archive')
|
||
|
||
league_data = data.get('league', {})
|
||
|
||
# Process league results using your existing function
|
||
calculate_league_final_scores(league_data)
|
||
participants = get_league_final_rankings(league_data)
|
||
|
||
# Use the existing league results display template but with archived data
|
||
return render_template('league_scoreboard_display.html',
|
||
league=league_data,
|
||
participants=participants,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at'),
|
||
'tournament_type': league_data.get('tournament_type', '20_targets')
|
||
})
|
||
|
||
# Mobile versions
|
||
@app.route('/mobile/archive/tournament/<filename>')
|
||
def mobile_view_archived_tournament(filename):
|
||
"""Mobile view of archived tournament"""
|
||
filepath = os.path.join(ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/mobile/archive')
|
||
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
# Process results for display
|
||
participants = []
|
||
for player_id, participant_data in results_data.get('participants', {}).items():
|
||
targets = participant_data.get('targets', {})
|
||
tens_count = calculate_tens_from_targets(targets)
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': participant_data['name'],
|
||
'total_score': participant_data['total_score'],
|
||
'completed': participant_data['completed'],
|
||
'tens_count': tens_count
|
||
})
|
||
|
||
# Sort by score (descending), then by tens (descending) as tiebreaker
|
||
participants.sort(key=lambda x: (x['total_score'], x['tens_count']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return render_template('mobile_results.html',
|
||
results=results_data,
|
||
participants=participants,
|
||
show_league_results=False,
|
||
show_tournament_results=True,
|
||
tournament_active=False,
|
||
league=None,
|
||
tournament_type=results_data.get('tournament_type', '20_targets'),
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at')
|
||
})
|
||
|
||
@app.route('/mobile/archive/league/<filename>')
|
||
def mobile_view_archived_league(filename):
|
||
"""Mobile view of archived league"""
|
||
filepath = os.path.join(LEAGUE_ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/mobile/archive')
|
||
|
||
league_data = data.get('league', {})
|
||
|
||
# Process league results
|
||
calculate_league_final_scores(league_data)
|
||
participants = get_league_final_rankings(league_data)
|
||
|
||
return render_template('mobile_results.html',
|
||
league=league_data,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=False,
|
||
results=None,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at')
|
||
})
|
||
# MAIN ROUTES
|
||
@app.route('/')
|
||
def index():
|
||
# Redirect mobile users to mobile menu
|
||
if is_mobile_device():
|
||
return redirect('/mobile')
|
||
|
||
# Desktop users get the regular dashboard
|
||
settings = load_settings()
|
||
|
||
# Check if tournament is active and get current round players
|
||
current_round_data = get_current_round_data()
|
||
|
||
# If tournament is active, override camera titles with player names
|
||
if current_round_data:
|
||
tournament_titles = {}
|
||
players = current_round_data['players']
|
||
|
||
for i in range(6): # Always 6 positions
|
||
if i < len(players):
|
||
tournament_titles[str(i + 1)] = players[i]['name']
|
||
else:
|
||
tournament_titles[str(i + 1)] = 'Empty'
|
||
|
||
# Create a copy of settings with tournament titles
|
||
display_settings = settings.copy()
|
||
display_settings['camera_titles'] = tournament_titles
|
||
display_settings['tournament_active'] = True
|
||
|
||
tournament_state = load_tournament_state()
|
||
display_settings['current_round'] = tournament_state.get('current_round', 1)
|
||
display_settings['total_rounds'] = tournament_state.get('total_rounds', 1)
|
||
|
||
# Add league info if available
|
||
league_state = load_league_state()
|
||
if league_state:
|
||
display_settings['league_active'] = True
|
||
display_settings['league_tournament'] = league_state.get('current_tournament', 1)
|
||
display_settings['league_total'] = league_state.get('total_tournaments', 6)
|
||
else:
|
||
display_settings = settings.copy()
|
||
display_settings['tournament_active'] = False
|
||
display_settings['current_round'] = 1
|
||
display_settings['total_rounds'] = 1
|
||
display_settings['league_active'] = False
|
||
|
||
|
||
return render_template('index.html',
|
||
streams=STREAMS,
|
||
settings=display_settings,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
# MOBILE ROUTES
|
||
@app.route('/mobile')
|
||
def mobile_menu():
|
||
"""Mobile redirect to streams"""
|
||
return redirect('/mobile/streams')
|
||
|
||
@app.route('/mobile/streams')
|
||
def mobile_streams():
|
||
"""Mobile streams page"""
|
||
settings = load_settings()
|
||
tournament_state = load_tournament_state()
|
||
|
||
# Check if tournament is active
|
||
tournament_active = tournament_state is not None
|
||
current_round_data = get_current_round_data() if tournament_active else None
|
||
|
||
return render_template('mobile_streams.html',
|
||
streams=STREAMS,
|
||
settings=settings,
|
||
tournament_active=tournament_active,
|
||
current_round_data=current_round_data,
|
||
tournament_state=tournament_state,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
@app.route('/mobile/draft')
|
||
def mobile_draft():
|
||
"""Mobile tournament draft page"""
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
if not tournament_state:
|
||
return redirect('/mobile')
|
||
|
||
return render_template('mobile_draft.html',
|
||
tournament=tournament_state,
|
||
league=league_state)
|
||
|
||
|
||
@app.route('/mobile/results')
|
||
def mobile_results():
|
||
"""Mobile results page"""
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
# Priority 1: Show league results if there's an active or finished league
|
||
if league_state:
|
||
if league_state.get('league_finished', False):
|
||
# Show final league results
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_final_rankings(league_state)
|
||
|
||
return render_template('mobile_results.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=False,
|
||
results=None)
|
||
else:
|
||
# Show ongoing league scoreboard
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_current_standings(league_state)
|
||
|
||
tournament_state = load_tournament_state()
|
||
tournament_active = tournament_state is not None
|
||
current_tournament_results = results
|
||
|
||
return render_template('mobile_league_results.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=tournament_active,
|
||
tournament_state=tournament_state,
|
||
current_tournament_results=current_tournament_results)
|
||
|
||
# Priority 1.5: Check if current results are from a finished league
|
||
elif results and results.get('league_tournament_number'):
|
||
# This is a league tournament result, but league state was archived
|
||
league_archives = get_archived_leagues()
|
||
|
||
if league_archives:
|
||
latest_archive = league_archives[0]
|
||
archive_data = load_archive_file(latest_archive['filepath'])
|
||
|
||
if archive_data and 'league' in archive_data:
|
||
archived_league = archive_data['league']
|
||
calculate_league_final_scores(archived_league)
|
||
participants = get_league_final_rankings(archived_league)
|
||
|
||
return render_template('mobile_results.html',
|
||
league=archived_league,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=False,
|
||
results=None,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': latest_archive['filename'],
|
||
'archived_at': archive_data.get('archived_at')
|
||
})
|
||
|
||
return redirect('/mobile/archive')
|
||
|
||
# Priority 2: Show individual tournament results (standalone tournament only)
|
||
elif results and results.get('tournament_finished', False):
|
||
participants = []
|
||
for player_id, data in results['participants'].items():
|
||
targets = data.get('targets', {})
|
||
tens_count = calculate_tens_from_targets(targets)
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'total_score': data['total_score'],
|
||
'completed': data['completed'],
|
||
'tens_count': tens_count
|
||
})
|
||
|
||
# Sort by score (descending), then by tens (descending) as tiebreaker
|
||
participants.sort(key=lambda x: (x['total_score'], x['tens_count']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return render_template('mobile_results.html',
|
||
results=results,
|
||
participants=participants,
|
||
show_league_results=False,
|
||
show_tournament_results=True,
|
||
tournament_active=False,
|
||
league=None,
|
||
tournament_type=results.get('tournament_type', '20_targets'))
|
||
else:
|
||
return redirect('/mobile')
|
||
|
||
# DESKTOP ROUTES
|
||
@app.route('/fullscreen/<int:camera_id>')
|
||
def fullscreen(camera_id):
|
||
# Get the camera stream data
|
||
if 1 <= camera_id <= len(STREAMS):
|
||
settings = load_settings()
|
||
stream = STREAMS[camera_id - 1]
|
||
|
||
# Check if tournament is active for title
|
||
current_round_data = get_current_round_data()
|
||
if current_round_data and camera_id <= len(current_round_data['players']):
|
||
camera_title = current_round_data['players'][camera_id - 1]['name']
|
||
else:
|
||
camera_title = settings['camera_titles'].get(str(camera_id), f'Camera {camera_id}')
|
||
|
||
custom_title = request.args.get('title', camera_title)
|
||
|
||
return render_template('fullscreen.html',
|
||
stream=stream,
|
||
camera_id=camera_id,
|
||
title=custom_title,
|
||
settings=settings)
|
||
else:
|
||
return redirect('/')
|
||
|
||
@app.route('/tournament')
|
||
def tournament():
|
||
"""Tournament management page"""
|
||
# Check if mobile device
|
||
if is_mobile_device():
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
if tournament_state:
|
||
# Mobile users with active tournament go to draft view
|
||
return redirect('/mobile/draft')
|
||
else:
|
||
# Mobile users without tournament go to mobile menu
|
||
return redirect('/mobile')
|
||
|
||
# Desktop users get full tournament management
|
||
players_data = load_players()
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
|
||
return render_template('tournament.html',
|
||
players=players_data['players'],
|
||
tournament_state=tournament_state,
|
||
league_state=league_state,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
@app.route('/tournament/draft')
|
||
def tournament_draft():
|
||
"""Tournament draft page"""
|
||
# Redirect mobile users to mobile draft
|
||
if is_mobile_device():
|
||
return redirect('/mobile/draft')
|
||
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
if not tournament_state:
|
||
return redirect('/tournament')
|
||
|
||
return render_template('draft.html',
|
||
tournament=tournament_state,
|
||
league=league_state)
|
||
|
||
@app.route('/results/calculator')
|
||
def results_calculator():
|
||
"""Results calculator page (desktop only)"""
|
||
if is_mobile_device():
|
||
return redirect('/mobile/streams')
|
||
|
||
tournament_state = load_tournament_state()
|
||
if not tournament_state:
|
||
return redirect('/tournament')
|
||
|
||
# Get or create results structure
|
||
results = load_results()
|
||
if not results:
|
||
results = create_results_structure(tournament_state)
|
||
save_results(results)
|
||
|
||
return render_template('results_calculator.html',
|
||
tournament=tournament_state,
|
||
results=results,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
@app.route('/results')
|
||
def results_display():
|
||
"""Results display page"""
|
||
# Redirect mobile users to mobile results
|
||
if is_mobile_device():
|
||
return redirect('/mobile/results')
|
||
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
# Priority 1: Show league results if there's an active or finished league
|
||
if league_state:
|
||
if league_state.get('league_finished', False):
|
||
# Show final league results
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_final_rankings(league_state)
|
||
|
||
return render_template('league_scoreboard_display.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
results=None,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
else:
|
||
# Show ongoing league scoreboard
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_current_standings(league_state)
|
||
|
||
tournament_state = load_tournament_state()
|
||
current_tournament_results = results
|
||
|
||
return render_template('league_scoreboard_display.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
tournament_state=tournament_state,
|
||
current_tournament_results=current_tournament_results,
|
||
results=None,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
|
||
# Priority 1.5: Check if current results are from a finished league (even if league state was archived)
|
||
elif results and results.get('league_tournament_number'):
|
||
# This is a league tournament result, but league state was archived
|
||
# Try to find the archived league data
|
||
league_archives = get_archived_leagues()
|
||
|
||
# Find the most recent league archive (should be the one that just finished)
|
||
if league_archives:
|
||
latest_archive = league_archives[0] # Already sorted by date (newest first)
|
||
archive_data = load_archive_file(latest_archive['filepath'])
|
||
|
||
if archive_data and 'league' in archive_data:
|
||
archived_league = archive_data['league']
|
||
|
||
# Show final league results from archive
|
||
calculate_league_final_scores(archived_league)
|
||
participants = get_league_final_rankings(archived_league)
|
||
|
||
return render_template('league_scoreboard_display.html',
|
||
league=archived_league,
|
||
participants=participants,
|
||
results=None,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': latest_archive['filename'],
|
||
'archived_at': archive_data.get('archived_at')
|
||
})
|
||
|
||
# If we can't find the archive, redirect to archive page
|
||
return redirect('/archive')
|
||
|
||
# Priority 2: Show individual tournament results (standalone tournament only)
|
||
elif results and results.get('tournament_finished', False):
|
||
participants = []
|
||
for player_id, data in results['participants'].items():
|
||
targets = data.get('targets', {})
|
||
tens_count = calculate_tens_from_targets(targets)
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'total_score': data['total_score'],
|
||
'completed': data['completed'],
|
||
'tens_count': tens_count
|
||
})
|
||
|
||
# Sort by score (descending), then by tens (descending) as tiebreaker
|
||
participants.sort(key=lambda x: (x['total_score'], x['tens_count']), reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return render_template('results_display.html',
|
||
results=results,
|
||
participants=participants,
|
||
league=None,
|
||
translations=get_translations(),
|
||
current_language=get_current_language())
|
||
else:
|
||
return redirect('/tournament')
|
||
|
||
# API Routes
|
||
@app.route('/api/settings', methods=['GET'])
|
||
def get_settings():
|
||
"""API endpoint to get current settings"""
|
||
settings = load_settings()
|
||
return jsonify(settings)
|
||
|
||
@app.route('/api/settings', methods=['POST'])
|
||
def update_settings():
|
||
"""API endpoint to update settings"""
|
||
try:
|
||
new_settings = request.get_json()
|
||
|
||
# Load current settings
|
||
current_settings = load_settings()
|
||
|
||
# Update only provided fields
|
||
if 'camera_titles' in new_settings:
|
||
current_settings['camera_titles'].update(new_settings['camera_titles'])
|
||
|
||
if 'display_options' in new_settings:
|
||
current_settings['display_options'].update(new_settings['display_options'])
|
||
|
||
# Save updated settings
|
||
if save_settings(current_settings):
|
||
return jsonify({'status': 'success', 'settings': current_settings})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save settings'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players', methods=['GET'])
|
||
def get_players():
|
||
"""API endpoint to get all players"""
|
||
players_data = load_players()
|
||
return jsonify(players_data)
|
||
|
||
@app.route('/api/players', methods=['POST'])
|
||
def update_players():
|
||
"""API endpoint to update players"""
|
||
try:
|
||
players_data = request.get_json()
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success', 'players': players_data})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save players'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players/add', methods=['POST'])
|
||
def add_player():
|
||
"""API endpoint to add a new player"""
|
||
try:
|
||
data = request.get_json()
|
||
name = data.get('name', '').strip()
|
||
|
||
if not name:
|
||
return jsonify({'status': 'error', 'message': 'Player name is required'}), 400
|
||
|
||
players_data = load_players()
|
||
|
||
# Find next available ID
|
||
existing_ids = [p['id'] for p in players_data['players']]
|
||
new_id = max(existing_ids) + 1 if existing_ids else 1
|
||
|
||
# Add new player
|
||
new_player = {
|
||
'id': new_id,
|
||
'name': name,
|
||
'enabled': True
|
||
}
|
||
|
||
players_data['players'].append(new_player)
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success', 'player': new_player})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save player'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players/<int:player_id>/delete', methods=['POST'])
|
||
def delete_player(player_id):
|
||
"""API endpoint to delete a player"""
|
||
try:
|
||
players_data = load_players()
|
||
|
||
# Find and remove the player
|
||
players_data['players'] = [p for p in players_data['players'] if p['id'] != player_id]
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success'})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save players'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players/<int:player_id>', methods=['POST'])
|
||
def update_player(player_id):
|
||
"""API endpoint to update a single player"""
|
||
try:
|
||
data = request.get_json()
|
||
players_data = load_players()
|
||
|
||
# Find and update the player
|
||
for player in players_data['players']:
|
||
if player['id'] == player_id:
|
||
if 'name' in data:
|
||
player['name'] = data['name']
|
||
if 'enabled' in data:
|
||
player['enabled'] = data['enabled']
|
||
break
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success', 'player': player})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save player'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
# LEAGUE API ROUTES
|
||
@app.route('/api/league/start', methods=['POST'])
|
||
def start_league():
|
||
"""API endpoint to start a new league"""
|
||
try:
|
||
data = request.get_json()
|
||
tournament_type = data.get('tournament_type', '20_targets')
|
||
if tournament_type not in ['20_targets', '40_targets', '4_targets']:
|
||
return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400
|
||
|
||
players_data = load_players()
|
||
enabled_players = [p for p in players_data['players'] if p['enabled']]
|
||
|
||
if len(enabled_players) < 1:
|
||
return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400
|
||
|
||
league_data = create_league(enabled_players, tournament_type)
|
||
|
||
if save_league_state(league_data):
|
||
return jsonify({'status': 'success', 'league': league_data})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save league'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/league/tournament/start', methods=['POST'])
|
||
def start_league_tournament():
|
||
"""API endpoint to start next tournament in league"""
|
||
try:
|
||
league_state = load_league_state()
|
||
if not league_state:
|
||
return jsonify({'status': 'error', 'message': 'No active league'}), 400
|
||
|
||
if league_state.get('league_finished', False):
|
||
return jsonify({'status': 'error', 'message': 'League already finished'}), 400
|
||
|
||
current_tournament = league_state.get('current_tournament', 0)
|
||
if current_tournament >= league_state['total_tournaments']:
|
||
return jsonify({'status': 'error', 'message': 'All tournaments completed'}), 400
|
||
|
||
# Get players for this tournament (excluding joker users for this round)
|
||
data = request.get_json() or {}
|
||
joker_players = data.get('joker_players', []) # List of player IDs using joker
|
||
|
||
# Update joker usage in league
|
||
for player_id in joker_players:
|
||
player_id_str = str(player_id)
|
||
if player_id_str in league_state['participants']:
|
||
if league_state['participants'][player_id_str]['joker_used']:
|
||
return jsonify({'status': 'error', 'message': f'Player {player_id} already used joker'}), 400
|
||
league_state['participants'][player_id_str]['joker_used'] = True
|
||
|
||
# Create list of participating players (not using joker)
|
||
participating_players = []
|
||
for player_id, participant in league_state['participants'].items():
|
||
if int(player_id) not in joker_players:
|
||
participating_players.append({
|
||
'id': int(player_id),
|
||
'name': participant['name']
|
||
})
|
||
|
||
if len(participating_players) < 1:
|
||
return jsonify({'status': 'error', 'message': 'Need at least 1 participating player'}), 400
|
||
|
||
# Start next tournament
|
||
league_state['current_tournament'] = current_tournament + 1
|
||
tournament_data = create_draft(
|
||
participating_players,
|
||
league_state['tournament_type'],
|
||
league_state['current_tournament']
|
||
)
|
||
|
||
# Save states
|
||
save_league_state(league_state)
|
||
|
||
if save_tournament_state(tournament_data):
|
||
# Create results structure
|
||
results = create_results_structure(tournament_data)
|
||
save_results(results)
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'tournament': tournament_data,
|
||
'league': league_state
|
||
})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/league/reset', methods=['POST'])
|
||
def reset_league():
|
||
"""API endpoint to reset/clear league"""
|
||
try:
|
||
# Archive current league if it exists
|
||
league_state = load_league_state()
|
||
if league_state:
|
||
archive_league(league_state)
|
||
|
||
# Remove league, tournament, and results files
|
||
for file_path in [LEAGUE_FILE, TOURNAMENT_FILE, RESULTS_FILE]:
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
|
||
return jsonify({'status': 'success'})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
# TOURNAMENT API ROUTES (Updated)
|
||
@app.route('/api/tournament/start', methods=['POST'])
|
||
def start_tournament():
|
||
"""API endpoint to start a standalone tournament"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
tournament_type = data.get('tournament_type', '20_targets')
|
||
|
||
if tournament_type not in ['20_targets', '40_targets', '4_targets']:
|
||
return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400
|
||
|
||
players_data = load_players()
|
||
enabled_players = [p for p in players_data['players'] if p['enabled']]
|
||
|
||
if len(enabled_players) < 1:
|
||
return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400
|
||
|
||
tournament_data = create_draft(enabled_players, tournament_type)
|
||
|
||
if save_tournament_state(tournament_data):
|
||
# Create results structure when tournament starts
|
||
results = create_results_structure(tournament_data)
|
||
save_results(results)
|
||
|
||
return jsonify({'status': 'success', 'tournament': tournament_data})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/tournament/round/<int:round_number>', methods=['POST'])
|
||
def change_round(round_number):
|
||
"""API endpoint to change current round"""
|
||
try:
|
||
tournament_state = load_tournament_state()
|
||
if not tournament_state:
|
||
return jsonify({'status': 'error', 'message': 'No active tournament'}), 400
|
||
|
||
if round_number < 1 or round_number > tournament_state['total_rounds']:
|
||
return jsonify({'status': 'error', 'message': 'Invalid round number'}), 400
|
||
|
||
tournament_state['current_round'] = round_number
|
||
|
||
# Update round statuses
|
||
for round_data in tournament_state['rounds']:
|
||
if round_data['round_number'] < round_number:
|
||
round_data['status'] = 'completed'
|
||
elif round_data['round_number'] == round_number:
|
||
round_data['status'] = 'pending'
|
||
else:
|
||
round_data['status'] = 'waiting'
|
||
|
||
if save_tournament_state(tournament_state):
|
||
return jsonify({'status': 'success', 'tournament': tournament_state})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/tournament/reset', methods=['POST'])
|
||
def reset_tournament():
|
||
"""API endpoint to reset/clear tournament"""
|
||
try:
|
||
if os.path.exists(TOURNAMENT_FILE):
|
||
os.remove(TOURNAMENT_FILE)
|
||
if os.path.exists(RESULTS_FILE):
|
||
os.remove(RESULTS_FILE)
|
||
return jsonify({'status': 'success'})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
# RESULTS API Routes (Updated)
|
||
@app.route('/api/results/participant/<int:player_id>', methods=['POST'])
|
||
def update_participant_scores(player_id):
|
||
"""API endpoint to update participant scores"""
|
||
try:
|
||
data = request.get_json()
|
||
results = load_results()
|
||
|
||
if not results:
|
||
return jsonify({'status': 'error', 'message': 'No results data found'}), 400
|
||
|
||
player_id_str = str(player_id)
|
||
if player_id_str not in results['participants']:
|
||
return jsonify({'status': 'error', 'message': 'Player not found'}), 400
|
||
|
||
# Update scores
|
||
if 'targets' in data:
|
||
results['participants'][player_id_str]['targets'].update(data['targets'])
|
||
|
||
# Recalculate total score
|
||
targets = results['participants'][player_id_str]['targets']
|
||
total_score = calculate_total_score(targets)
|
||
results['participants'][player_id_str]['total_score'] = total_score
|
||
|
||
# Check if all targets are completed (all shots entered, including 0s)
|
||
all_completed = is_participant_completed(targets)
|
||
results['participants'][player_id_str]['completed'] = all_completed
|
||
|
||
if save_results(results):
|
||
return jsonify({'status': 'success', 'participant': results['participants'][player_id_str]})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save results'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/results/finish', methods=['POST'])
|
||
def finish_tournament():
|
||
"""API endpoint to finish tournament"""
|
||
try:
|
||
results = load_results()
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
|
||
if not results:
|
||
return jsonify({'status': 'error', 'message': 'No results data found'}), 400
|
||
|
||
if not tournament_state:
|
||
return jsonify({'status': 'error', 'message': 'No active tournament found'}), 400
|
||
|
||
# Check if all participants are completed
|
||
all_completed = all(
|
||
participant['completed']
|
||
for participant in results['participants'].values()
|
||
)
|
||
|
||
if not all_completed:
|
||
return jsonify({'status': 'error', 'message': 'Not all participants have completed scores'}), 400
|
||
|
||
# Mark tournament as finished
|
||
results['tournament_finished'] = True
|
||
results['finished_at'] = datetime.now().isoformat()
|
||
|
||
# Define total shots per participant for each tournament type
|
||
total_shots_per_participant = {
|
||
'20_targets': 40, # 20 targets × 2 shots
|
||
'40_targets': 80, # 40 targets × 2 shots
|
||
'4_targets': 20 # 4 targets × 5 shots
|
||
}
|
||
|
||
league_finished = False # Track if league finished
|
||
|
||
# Update league state if this is a league tournament
|
||
if league_state and 'league_tournament_number' in results:
|
||
tournament_number = results['league_tournament_number']
|
||
|
||
# Update league with tournament results
|
||
for player_id, participant in results['participants'].items():
|
||
if player_id in league_state['participants']:
|
||
league_participant = league_state['participants'][player_id]
|
||
|
||
# Calculate 10s using existing logic
|
||
tens_count = 0
|
||
targets = participant.get('targets', {})
|
||
for target in targets.values():
|
||
for shot_key, shot_value in target.items():
|
||
if shot_key.startswith('shot') and shot_value == 10:
|
||
tens_count += 1
|
||
|
||
# Add tournament result with 10s count
|
||
league_participant['tournament_results'].append({
|
||
'tournament': tournament_number,
|
||
'score': participant['total_score'],
|
||
'tens_count': tens_count,
|
||
'participated': True
|
||
})
|
||
|
||
# Update total score
|
||
league_participant['total_score'] += participant['total_score']
|
||
|
||
# Add results for players who used joker (didn't participate)
|
||
for player_id, participant in league_state['participants'].items():
|
||
if player_id not in results['participants']:
|
||
# This player used joker
|
||
participant['tournament_results'].append({
|
||
'tournament': tournament_number,
|
||
'score': 0,
|
||
'tens_count': 0,
|
||
'participated': False,
|
||
'joker': True
|
||
})
|
||
|
||
# Calculate total shots correctly for any tournament type
|
||
tournament_type = results.get('tournament_type', '20_targets')
|
||
shots_per_participant = total_shots_per_participant.get(tournament_type, 40)
|
||
total_shots_fired = len(results['participants']) * shots_per_participant
|
||
|
||
# Add to completed tournaments
|
||
league_state['completed_tournaments'].append({
|
||
'tournament_number': tournament_number,
|
||
'tournament_type': tournament_type,
|
||
'finished_at': datetime.now().isoformat(),
|
||
'results_summary': {
|
||
'participants': len(results['participants']),
|
||
'shots_per_participant': shots_per_participant,
|
||
'total_shots': total_shots_fired,
|
||
'format_description': get_tournament_format_description(tournament_type)
|
||
}
|
||
})
|
||
|
||
# Check if league is finished
|
||
if tournament_number >= league_state['total_tournaments']:
|
||
league_state['league_finished'] = True
|
||
league_state['finished_at'] = datetime.now().isoformat()
|
||
|
||
# Calculate final scores
|
||
calculate_league_final_scores(league_state)
|
||
|
||
league_finished = True
|
||
print(f"League finished! Final tournament was {tournament_type} format.")
|
||
|
||
save_league_state(league_state)
|
||
|
||
# Archive the tournament (only if it's NOT part of a league)
|
||
archive_success = False
|
||
if not league_state: # Only archive standalone tournaments
|
||
archive_success = archive_tournament(tournament_state, results)
|
||
tournament_type = results.get('tournament_type', '20_targets')
|
||
print(f"Standalone {tournament_type} tournament archived: {archive_success}")
|
||
else:
|
||
tournament_type = results.get('tournament_type', '20_targets')
|
||
print(f"League {tournament_type} tournament - not archiving individual tournament")
|
||
|
||
# Archive the league if it just finished
|
||
league_archive_success = False
|
||
if league_finished and league_state:
|
||
league_archive_success = archive_league(league_state)
|
||
print(f"League archived: {league_archive_success}")
|
||
|
||
# Save final results
|
||
if save_results(results):
|
||
# End the tournament by removing tournament state
|
||
if os.path.exists(TOURNAMENT_FILE):
|
||
os.remove(TOURNAMENT_FILE)
|
||
|
||
# If league finished, also remove league state file
|
||
if league_finished and os.path.exists(LEAGUE_FILE):
|
||
os.remove(LEAGUE_FILE)
|
||
|
||
response_data = {
|
||
'status': 'success',
|
||
'results': results,
|
||
'archived': archive_success,
|
||
'league_archived': league_archive_success if league_finished else None,
|
||
'tournament_type': results.get('tournament_type', '20_targets'),
|
||
'tournament_format': get_tournament_format_description(results.get('tournament_type', '20_targets'))
|
||
}
|
||
|
||
if league_state:
|
||
response_data['league'] = league_state
|
||
response_data['league_finished'] = league_state.get('league_finished', False)
|
||
|
||
return jsonify(response_data)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save results'}), 500
|
||
|
||
except Exception as e:
|
||
print(f"Error finishing tournament: {e}")
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
|
||
def get_tournament_format_description(tournament_type):
|
||
"""Get human-readable description of tournament format"""
|
||
format_descriptions = {
|
||
'20_targets': '20 Targets (2 shots each)',
|
||
'40_targets': '40 Targets (2 shots each)',
|
||
'4_targets': '4 Targets (5 shots each)'
|
||
}
|
||
return format_descriptions.get(tournament_type, '20 Targets (2 shots each)')
|
||
@app.route('/api/results', methods=['GET'])
|
||
def get_results():
|
||
"""API endpoint to get current results"""
|
||
results = load_results()
|
||
if results:
|
||
return jsonify(results)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'No results found'}), 404
|
||
|
||
@app.route('/api/league', methods=['GET'])
|
||
def get_league():
|
||
"""API endpoint to get current league state"""
|
||
league_state = load_league_state()
|
||
if league_state:
|
||
return jsonify(league_state)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'No league found'}), 404
|
||
|
||
# Add this route to your Flask app (around line 850, with the other mobile routes)
|
||
|
||
@app.route('/mobile/remote')
|
||
def mobile_remote():
|
||
"""Mobile remote control page"""
|
||
# This page doesn't redirect mobile users - it's specifically for mobile control
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
return render_template('mobile_remote.html',
|
||
tournament_state=tournament_state,
|
||
league_state=league_state,
|
||
results=results)
|
||
|
||
# Add these API endpoints for the remote control functionality
|
||
|
||
@app.route('/api/tournament', methods=['GET'])
|
||
def get_tournament():
|
||
"""API endpoint to get current tournament state"""
|
||
tournament_state = load_tournament_state()
|
||
if tournament_state:
|
||
return jsonify(tournament_state)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'No tournament found'}), 404
|
||
|
||
@app.route('/api/remote/refresh_dashboard', methods=['POST'])
|
||
def refresh_dashboard():
|
||
"""API endpoint to trigger dashboard refresh"""
|
||
try:
|
||
# You could add logic here to trigger refresh on connected clients
|
||
# For now, just return success
|
||
return jsonify({'status': 'success', 'message': 'Refresh signal sent'})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/remote/system_info', methods=['GET'])
|
||
def get_system_info():
|
||
"""API endpoint to get comprehensive system information"""
|
||
try:
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
players_data = load_players()
|
||
settings = load_settings()
|
||
|
||
system_info = {
|
||
'timestamp': datetime.now().isoformat(),
|
||
'tournament': {
|
||
'active': tournament_state is not None,
|
||
'data': tournament_state
|
||
},
|
||
'league': {
|
||
'active': league_state is not None and not league_state.get('league_finished', False),
|
||
'finished': league_state is not None and league_state.get('league_finished', False),
|
||
'data': league_state
|
||
},
|
||
'results': {
|
||
'available': results is not None,
|
||
'finished': results is not None and results.get('tournament_finished', False),
|
||
'data': results
|
||
},
|
||
'players': {
|
||
'total': len(players_data['players']) if players_data else 0,
|
||
'enabled': len([p for p in players_data['players'] if p['enabled']]) if players_data else 0
|
||
},
|
||
'settings': settings
|
||
}
|
||
|
||
return jsonify(system_info)
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/remote/emergency_reset', methods=['POST'])
|
||
def emergency_reset():
|
||
"""API endpoint for emergency system reset"""
|
||
try:
|
||
# Archive current states before reset
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
if tournament_state and results:
|
||
archive_tournament(tournament_state, results)
|
||
|
||
if league_state:
|
||
archive_league(league_state)
|
||
|
||
# Remove all state files
|
||
for file_path in [LEAGUE_FILE, TOURNAMENT_FILE, RESULTS_FILE]:
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'message': 'Emergency reset completed',
|
||
'archived': True
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/remote/camera_titles', methods=['GET'])
|
||
def get_camera_titles():
|
||
"""API endpoint to get camera titles"""
|
||
try:
|
||
settings = load_settings()
|
||
current_round_data = get_current_round_data()
|
||
|
||
titles = {}
|
||
|
||
if current_round_data:
|
||
# Tournament active - use player names
|
||
players = current_round_data['players']
|
||
for i in range(6):
|
||
if i < len(players):
|
||
titles[str(i + 1)] = players[i]['name']
|
||
else:
|
||
titles[str(i + 1)] = 'Empty'
|
||
else:
|
||
# No tournament - use configured titles
|
||
titles = settings['camera_titles']
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'titles': titles,
|
||
'tournament_active': current_round_data is not None
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/archive/player/<int:player_id>/shot-accuracy')
|
||
def get_player_shot_accuracy(player_id):
|
||
"""
|
||
Get aggregated shot accuracy data for a player from tournament archive files
|
||
"""
|
||
try:
|
||
# Get player name from JSON file instead of database
|
||
players_data = load_players()
|
||
player = None
|
||
for p in players_data['players']:
|
||
if p['id'] == player_id:
|
||
player = p
|
||
break
|
||
|
||
if not player:
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': f'Player with ID {player_id} not found'
|
||
}), 404
|
||
|
||
player_name = player['name']
|
||
|
||
# Initialize shot counts by tournament type
|
||
shot_accuracy = {
|
||
'40 Targets': defaultdict(int),
|
||
'20 Targets': defaultdict(int),
|
||
'4 Targets': defaultdict(int)
|
||
}
|
||
|
||
# Path to tournament archives
|
||
tournament_archives_path = 'tournament_archives'
|
||
|
||
# Check if directory exists
|
||
if not os.path.exists(tournament_archives_path):
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': f'Tournament archives directory not found: {tournament_archives_path}'
|
||
}), 404
|
||
|
||
# Find all tournament archive files
|
||
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
|
||
|
||
if not archive_files:
|
||
return jsonify({
|
||
'status': 'success',
|
||
'data': {},
|
||
'player_name': player_name,
|
||
'message': 'No tournament archive files found'
|
||
})
|
||
|
||
# Process each tournament archive file
|
||
for file_path in archive_files:
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
tournament_data = json.load(f)
|
||
|
||
# Check if this tournament has our player
|
||
participants = tournament_data.get('results', {}).get('participants', {})
|
||
|
||
# Find player by ID or name
|
||
player_data = None
|
||
for participant_id, participant_info in participants.items():
|
||
if (str(participant_id) == str(player_id) or
|
||
participant_info.get('name') == player_name):
|
||
player_data = participant_info
|
||
break
|
||
|
||
if not player_data or not player_data.get('completed'):
|
||
continue
|
||
|
||
# Determine tournament type
|
||
tournament_type = determine_tournament_type_from_archive(tournament_data)
|
||
|
||
# Extract individual shots - NOW WITH PROPER SHOT COUNTING FOR ALL FORMATS
|
||
shots = extract_shots_from_player_data(player_data, tournament_type)
|
||
|
||
# Debug print to verify shot counts
|
||
print(f"Player {player_name}, Tournament type: {tournament_type}, Total shots extracted: {len(shots)}")
|
||
|
||
# Count shots by value
|
||
for shot_value in shots:
|
||
if shot_value == 10:
|
||
shot_accuracy[tournament_type]['tens'] += 1
|
||
elif shot_value == 9:
|
||
shot_accuracy[tournament_type]['nines'] += 1
|
||
elif shot_value == 8:
|
||
shot_accuracy[tournament_type]['eights'] += 1
|
||
elif shot_value == 7:
|
||
shot_accuracy[tournament_type]['sevens'] += 1
|
||
elif shot_value == 6:
|
||
shot_accuracy[tournament_type]['sixes'] += 1
|
||
elif shot_value == 5:
|
||
shot_accuracy[tournament_type]['fives'] += 1
|
||
elif shot_value == 4:
|
||
shot_accuracy[tournament_type]['fours'] += 1
|
||
elif shot_value == 3:
|
||
shot_accuracy[tournament_type]['threes'] += 1
|
||
elif shot_value == 2:
|
||
shot_accuracy[tournament_type]['twos'] += 1
|
||
elif shot_value == 1:
|
||
shot_accuracy[tournament_type]['ones'] += 1
|
||
elif shot_value == 0:
|
||
shot_accuracy[tournament_type]['zeros'] += 1
|
||
|
||
except Exception as e:
|
||
print(f"Error processing tournament file {file_path}: {e}")
|
||
continue
|
||
|
||
# Convert defaultdict to regular dict for JSON serialization
|
||
result = {}
|
||
for tournament_type, counts in shot_accuracy.items():
|
||
if any(counts.values()): # Only include types with data
|
||
result[tournament_type] = dict(counts)
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'data': result,
|
||
'player_name': player_name,
|
||
'files_processed': len(archive_files)
|
||
})
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': str(e),
|
||
'traceback': traceback.format_exc()
|
||
}), 500
|
||
|
||
@app.route('/api/archive/tournament/<tournament_id>/shots')
|
||
def get_tournament_shots(tournament_id):
|
||
"""
|
||
Get individual shot data for a specific tournament from archive
|
||
"""
|
||
try:
|
||
# Tournament ID might be a database ID or the tournament filename/timestamp
|
||
tournament_archives_path = 'tournament_archives'
|
||
|
||
# Try to find tournament file by various methods
|
||
tournament_file = None
|
||
|
||
# Method 1: Direct filename match
|
||
potential_files = [
|
||
f"{tournament_archives_path}/tournament_{tournament_id}.json",
|
||
f"{tournament_archives_path}/{tournament_id}.json"
|
||
]
|
||
|
||
for file_path in potential_files:
|
||
if os.path.exists(file_path):
|
||
tournament_file = file_path
|
||
break
|
||
|
||
# Method 2: Search through all tournament files for matching ID
|
||
if not tournament_file:
|
||
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
|
||
for file_path in archive_files:
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
# Check if tournament ID matches
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
if (tournament_data.get('created_at') == tournament_id or
|
||
results_data.get('tournament_id') == tournament_id or
|
||
str(tournament_id) in file_path):
|
||
tournament_file = file_path
|
||
break
|
||
except:
|
||
continue
|
||
|
||
if not tournament_file:
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': 'Tournament archive file not found'
|
||
}), 404
|
||
|
||
# Load tournament data
|
||
with open(tournament_file, 'r') as f:
|
||
tournament_data = json.load(f)
|
||
|
||
# Extract all players' shots
|
||
participants = tournament_data.get('results', {}).get('participants', {})
|
||
all_shots_data = {}
|
||
|
||
for participant_id, participant_info in participants.items():
|
||
if participant_info.get('completed'):
|
||
shots = extract_shots_from_player_data(participant_info)
|
||
all_shots_data[participant_info.get('name')] = shots
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'tournament_data': {
|
||
'tournament_type': determine_tournament_type_from_archive(tournament_data),
|
||
'participants': len(participants),
|
||
'file_path': tournament_file
|
||
},
|
||
'shots_by_player': all_shots_data
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': str(e)
|
||
}), 500
|
||
|
||
|
||
def extract_shots_from_player_data(player_data, tournament_type=None):
|
||
"""
|
||
Extract individual shot values from player data in your archive format
|
||
Now properly handles all tournament formats:
|
||
- 4 targets: 5 shots each (shot1-shot5) = 20 total shots
|
||
- 20 targets: 2 shots each (shot1-shot2) = 40 total shots
|
||
- 40 targets: 2 shots each (shot1-shot2) = 80 total shots
|
||
"""
|
||
shots = []
|
||
targets = player_data.get('targets', {})
|
||
|
||
if not targets:
|
||
return shots
|
||
|
||
# Sort targets by number to maintain order
|
||
target_numbers = sorted([int(k) for k in targets.keys() if k.isdigit()])
|
||
|
||
# Determine shots per target based on tournament format
|
||
if tournament_type and '4' in str(tournament_type):
|
||
shots_per_target = 5 # 4 targets format: 5 shots each
|
||
else:
|
||
# Auto-detect if tournament_type not provided
|
||
num_targets = len(target_numbers)
|
||
if num_targets <= 6: # Likely 4 targets format (4 targets + maybe some extras)
|
||
shots_per_target = 5
|
||
else: # 20 or 40 targets format
|
||
shots_per_target = 2
|
||
|
||
for target_num in target_numbers:
|
||
target_data = targets[str(target_num)]
|
||
|
||
# Extract shots for this target
|
||
for shot_num in range(1, shots_per_target + 1):
|
||
shot_key = f'shot{shot_num}'
|
||
shot_value = target_data.get(shot_key)
|
||
|
||
if shot_value is not None:
|
||
shots.append(int(shot_value))
|
||
|
||
return shots
|
||
|
||
def count_shots_in_tournament(participant_data, tournament_type=None):
|
||
"""
|
||
Count total shots fired by a participant in a tournament
|
||
Now properly handles all tournament formats:
|
||
- 4 targets: 5 shots each = 20 total shots
|
||
- 20 targets: 2 shots each = 40 total shots
|
||
- 40 targets: 2 shots each = 80 total shots
|
||
"""
|
||
targets = participant_data.get('targets', {})
|
||
shots_count = 0
|
||
|
||
if not targets:
|
||
return shots_count
|
||
|
||
# Determine shots per target based on tournament format
|
||
if tournament_type and '4' in str(tournament_type):
|
||
max_shots_per_target = 5 # 4 targets format: 5 shots each
|
||
else:
|
||
# Auto-detect if tournament_type not provided
|
||
target_count = len([k for k in targets.keys() if k.isdigit()])
|
||
if target_count <= 6: # Likely 4 targets format
|
||
max_shots_per_target = 5
|
||
else: # 20 or 40 targets format
|
||
max_shots_per_target = 2
|
||
|
||
for target in targets.values():
|
||
for shot_num in range(1, max_shots_per_target + 1):
|
||
shot_key = f'shot{shot_num}'
|
||
if target.get(shot_key) is not None:
|
||
shots_count += 1
|
||
|
||
return shots_count
|
||
|
||
def determine_tournament_type_from_archive(tournament_data):
|
||
"""
|
||
Determine tournament type from your archive data structure
|
||
"""
|
||
# First check the tournament_type field
|
||
tournament_info = tournament_data.get('tournament', {})
|
||
results_info = tournament_data.get('results', {})
|
||
|
||
tournament_type = (tournament_info.get('tournament_type') or
|
||
results_info.get('tournament_type'))
|
||
|
||
if tournament_type:
|
||
if '40' in tournament_type:
|
||
return '40 Targets'
|
||
elif '20' in tournament_type:
|
||
return '20 Targets'
|
||
elif '4' in tournament_type:
|
||
return '4 Targets'
|
||
|
||
# Fallback: count targets from first completed player
|
||
participants = results_info.get('participants', {})
|
||
for participant_info in participants.values():
|
||
if participant_info.get('completed'):
|
||
targets = participant_info.get('targets', {})
|
||
target_count = len([k for k in targets.keys() if k.isdigit()])
|
||
|
||
if target_count >= 30: # 40 targets
|
||
return '40 Targets'
|
||
elif target_count >= 10: # 20 targets
|
||
return '20 Targets'
|
||
elif target_count <= 6: # 4 targets (changed from <= 8 to <= 6)
|
||
return '4 Targets'
|
||
break
|
||
|
||
# Default fallback
|
||
return '20 Targets'
|
||
|
||
@app.route('/api/debug/player/<int:player_id>')
|
||
def debug_player_info(player_id):
|
||
"""
|
||
Debug endpoint to check player info and archive structure
|
||
"""
|
||
try:
|
||
# Check player exists
|
||
players_data = load_players()
|
||
player = None
|
||
for p in players_data['players']:
|
||
if p['id'] == player_id:
|
||
player = p
|
||
break
|
||
|
||
# Check archive directory
|
||
tournament_archives_path = 'tournament_archives'
|
||
archive_exists = os.path.exists(tournament_archives_path)
|
||
archive_files = []
|
||
|
||
if archive_exists:
|
||
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
|
||
|
||
# Check current working directory
|
||
cwd = os.getcwd()
|
||
|
||
# List contents of current directory
|
||
current_dir_contents = os.listdir('.')
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'player_found': player is not None,
|
||
'player_info': player,
|
||
'current_working_directory': cwd,
|
||
'current_dir_contents': current_dir_contents,
|
||
'archive_directory_exists': archive_exists,
|
||
'archive_directory_path': tournament_archives_path,
|
||
'archive_files_found': len(archive_files),
|
||
'archive_files': [os.path.basename(f) for f in archive_files[:5]] # First 5 files
|
||
})
|
||
|
||
except Exception as e:
|
||
import traceback
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': str(e),
|
||
'traceback': traceback.format_exc()
|
||
}), 500
|
||
|
||
# Debug endpoint to see raw tournament data
|
||
@app.route('/api/archive/tournament-file/<path:filename>')
|
||
def get_tournament_file_debug(filename):
|
||
"""
|
||
Debug endpoint to see raw tournament file data
|
||
"""
|
||
try:
|
||
tournament_archives_path = 'tournament_archives'
|
||
file_path = os.path.join(tournament_archives_path, filename)
|
||
|
||
if not os.path.exists(file_path):
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': 'File not found'
|
||
}), 404
|
||
|
||
with open(file_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'file_path': file_path,
|
||
'data': data
|
||
})
|
||
|
||
except Exception as e:
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': str(e)
|
||
}), 500
|
||
|
||
@app.route('/api/archive/tournament-leaders', methods=['GET'])
|
||
def api_get_tournament_leaders():
|
||
"""API endpoint to get overall tournament leaders by tournament type"""
|
||
try:
|
||
tournaments = get_archived_tournaments()
|
||
|
||
# Group data by tournament type
|
||
tournament_types = {
|
||
'20_targets': {
|
||
'name': '20 Targets',
|
||
'description': '20 Targets (2 shots each)',
|
||
'best_score': {'player_name': None, 'score': 0},
|
||
'most_tens': {'player_name': None, 'tens': 0},
|
||
'total_tournaments': 0
|
||
},
|
||
'40_targets': {
|
||
'name': '40 Targets',
|
||
'description': '40 Targets (2 shots each)',
|
||
'best_score': {'player_name': None, 'score': 0},
|
||
'most_tens': {'player_name': None, 'tens': 0},
|
||
'total_tournaments': 0
|
||
},
|
||
'4_targets': {
|
||
'name': '4 Targets',
|
||
'description': '4 Targets (5 shots each)',
|
||
'best_score': {'player_name': None, 'score': 0},
|
||
'most_tens': {'player_name': None, 'tens': 0},
|
||
'total_tournaments': 0
|
||
}
|
||
}
|
||
|
||
for tournament in tournaments:
|
||
try:
|
||
data = load_archive_file(tournament['filepath'])
|
||
if not data:
|
||
continue
|
||
|
||
results = data.get('results', {})
|
||
participants = results.get('participants', {})
|
||
tournament_type = tournament.get('tournament_type', '20_targets')
|
||
|
||
if tournament_type not in tournament_types or not participants:
|
||
continue
|
||
|
||
tournament_types[tournament_type]['total_tournaments'] += 1
|
||
|
||
for player_id, participant in participants.items():
|
||
if not participant.get('completed'):
|
||
continue
|
||
|
||
player_name = participant.get('name', f'Player {player_id}')
|
||
|
||
# Check best score for this tournament type
|
||
score = participant.get('total_score', 0)
|
||
if score > tournament_types[tournament_type]['best_score']['score']:
|
||
tournament_types[tournament_type]['best_score'] = {
|
||
'player_name': player_name,
|
||
'score': score
|
||
}
|
||
|
||
# Count 10s for this player in this tournament
|
||
targets = participant.get('targets', {})
|
||
tens_count = 0
|
||
for target in targets.values():
|
||
if target.get('shot1') == 10:
|
||
tens_count += 1
|
||
if target.get('shot2') == 10:
|
||
tens_count += 1
|
||
# For 4_targets format, check additional shots
|
||
for shot_num in range(3, 6): # shot3, shot4, shot5
|
||
if target.get(f'shot{shot_num}') == 10:
|
||
tens_count += 1
|
||
|
||
if tens_count > tournament_types[tournament_type]['most_tens']['tens']:
|
||
tournament_types[tournament_type]['most_tens'] = {
|
||
'player_name': player_name,
|
||
'tens': tens_count
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"Error processing tournament {tournament.get('filepath', 'unknown')}: {e}")
|
||
continue
|
||
|
||
# Convert to list format, only include types with data
|
||
tournament_leaders = []
|
||
for type_key, type_data in tournament_types.items():
|
||
if type_data['total_tournaments'] > 0 and type_data['best_score']['player_name']:
|
||
tournament_leaders.append({
|
||
'id': type_key,
|
||
'name': type_data['name'],
|
||
'description': type_data['description'],
|
||
'total_tournaments': type_data['total_tournaments'],
|
||
'best_score': type_data['best_score'],
|
||
'most_tens': type_data['most_tens']
|
||
})
|
||
|
||
return jsonify({'status': 'success', 'tournament_types': tournament_leaders})
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||
|
||
# Language API endpoints
|
||
@app.route('/api/language', methods=['GET'])
|
||
def get_language():
|
||
"""Get current language"""
|
||
return jsonify({
|
||
'current_language': get_current_language(),
|
||
'supported_languages': SUPPORTED_LANGUAGES,
|
||
'translations': get_translations()
|
||
})
|
||
|
||
@app.route('/api/language/<language>', methods=['POST'])
|
||
def set_language(language):
|
||
"""Set current language"""
|
||
if language in SUPPORTED_LANGUAGES:
|
||
session['language'] = language
|
||
return jsonify({
|
||
'status': 'success',
|
||
'language': language,
|
||
'translations': get_translations()
|
||
})
|
||
else:
|
||
return jsonify({
|
||
'status': 'error',
|
||
'message': f'Unsupported language: {language}'
|
||
}), 400
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host='0.0.0.0', port=5000, debug=True) |