""" TV_APP V1.0.0 - Tournament and League Management System Flask web application for managing tournaments with multi-camera streaming """ 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 # Import modular components from app.utils import ( load_translations, get_current_language, get_translations, is_mobile_device, calculate_tens_from_targets ) from app.storage import ( SettingsStorage, PlayerStorage, TournamentStorage, ResultsStorage, LeagueStorage, ArchiveStorage, TOURNAMENT_FILE, RESULTS_FILE, LEAGUE_FILE ) from app.models import Tournament, Scoring, RoundManager # Initialize Flask app app = Flask(__name__) app.secret_key = 'your-secret-key-for-sessions' app.debug = False # Configuration: Camera Streams (6 targets) 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'}, ] # Language Configuration SUPPORTED_LANGUAGES = ['sl', 'en'] DEFAULT_LANGUAGE = 'sl' # Data file paths (organized in data directory) ARCHIVE_DIR = 'data/tournament_archives' LEAGUE_ARCHIVE_DIR = 'data/league_archives' # Convenience function wrappers that delegate to storage classes def load_settings(): return SettingsStorage.load_settings() def save_settings(settings): return SettingsStorage.save_settings(settings) def load_players(): return PlayerStorage.load_players() def save_players(players_data): return PlayerStorage.save_players(players_data) def load_tournament_state(): return TournamentStorage.load_tournament_state() def save_tournament_state(tournament_data): return TournamentStorage.save_tournament_state(tournament_data) def load_league_state(): return LeagueStorage.load_league_state() def save_league_state(league_data): return LeagueStorage.save_league_state(league_data) def load_results(): return ResultsStorage.load_results() def save_results(results_data): return ResultsStorage.save_results(results_data) def archive_tournament(tournament_data, results_data): return ArchiveStorage.archive_tournament(tournament_data, results_data) def archive_league(league_data): return ArchiveStorage.archive_league(league_data) # Convenience wrappers that delegate to modular classes def create_league(enabled_players, tournament_type): return Tournament.create_league(enabled_players, tournament_type) def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None): return Tournament.create_draft(enabled_players, tournament_type, league_tournament_number) def create_results_structure(tournament_data): return Tournament.create_results_structure(tournament_data) def calculate_total_score(targets): return Scoring.calculate_total_score(targets) def is_participant_completed(targets): return Scoring.is_participant_completed(targets) def calculate_league_final_scores(league_data): return Scoring.calculate_league_final_scores(league_data) def get_league_final_rankings(league_data): return Scoring.get_league_final_rankings(league_data) def calculate_current_league_standings(league_data): return Scoring.calculate_current_league_standings(league_data) def get_current_round_data(): tournament_state = load_tournament_state() return RoundManager.get_current_round_data(tournament_state) def get_league_current_standings(league_state): """Get current league standings including in-progress tournament""" if not league_state: return [] participants = [] for player_id, data in league_state['participants'].items(): 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, 'current_tournament_participating': False } participants.append(participant) 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(): for participant in participants: if participant['id'] == player_id: participant['current_tournament_score'] = result_data['total_score'] participant['current_tournament_participating'] = True break participants.sort(key=lambda x: (x['final_score'], x['total_tens']), reverse=True) for i, participant in enumerate(participants): participant['rank'] = i + 1 return participants # Archive functions def get_archived_tournaments(): return ArchiveStorage.get_archived_tournaments() def get_archived_leagues(): return ArchiveStorage.get_archived_leagues() def load_archive_file(filepath): return ArchiveStorage.load_archive_file(filepath) def calculate_shot_accuracy(participant): """ Extract shot accuracy breakdown from participant targets. Returns dict with counts for each shot value (0-10). """ accuracy_data = { 'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0 } targets = participant.get('targets', {}) for target_num, shots in targets.items(): # Extract all shots from this target (shot1, shot2, etc.) for shot_key, shot_value in shots.items(): shot_val = int(shot_value) if shot_val == 10: accuracy_data['tens'] += 1 elif shot_val == 9: accuracy_data['nines'] += 1 elif shot_val == 8: accuracy_data['eights'] += 1 elif shot_val == 7: accuracy_data['sevens'] += 1 elif shot_val == 6: accuracy_data['sixes'] += 1 elif shot_val == 5: accuracy_data['fives'] += 1 elif shot_val == 4: accuracy_data['fours'] += 1 elif shot_val == 3: accuracy_data['threes'] += 1 elif shot_val == 2: accuracy_data['twos'] += 1 elif shot_val == 1: accuracy_data['ones'] += 1 elif shot_val == 0: accuracy_data['zeros'] += 1 return accuracy_data 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': [], 'shot_accuracy': { '4_targets': {'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0}, '20_targets': {'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0}, '40_targets': {'tens': 0, 'nines': 0, 'eights': 0, 'sevens': 0, 'sixes': 0, 'fives': 0, 'fours': 0, 'threes': 0, 'twos': 0, 'ones': 0, 'zeros': 0} } } # 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 # Calculate and aggregate shot accuracy if tournament_type in player_stats['shot_accuracy']: shot_accuracy = calculate_shot_accuracy(participant) for key in shot_accuracy: player_stats['shot_accuracy'][tournament_type][key] += shot_accuracy[key] # 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 'filename': archive.get('filename', '') }) 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) tournament_results = participant.get('tournament_results', []) # Count tournaments participated based on tournament_results array tournaments_participated = len([t for t in tournament_results if t.get('participated', False)]) 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': tournament_results, 'filename': archive.get('filename', ''), 'excluded_tournament': participant.get('excluded_tournament', None) }) 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 # ============================================================================ # Flask Routes # ============================================================================ # 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/') 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/') 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/') 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/') 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/') 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: # Use "Prazno" for Slovenian, "Empty" for English current_language = get_current_language() tournament_titles[str(i + 1)] = 'Prazno' if current_language == 'sl' else '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 # Check if league is active when tournament is not 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['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/') 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//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/', 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) # Unpack tuple but ignore values # 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/', methods=['POST']) def change_round(round_number): """API endpoint to change current round""" try: print(f"[ROUND CHANGE] Attempting to change to round {round_number}") tournament_state = load_tournament_state() if not tournament_state: print(f"[ROUND CHANGE] Error: No active tournament") return jsonify({'status': 'error', 'message': 'No active tournament'}), 400 print(f"[ROUND CHANGE] Tournament found. Total rounds: {tournament_state.get('total_rounds')}") if round_number < 1 or round_number > tournament_state['total_rounds']: print(f"[ROUND CHANGE] Error: Invalid round number {round_number}") 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' print(f"[ROUND CHANGE] Saving tournament state...") if save_tournament_state(tournament_state): print(f"[ROUND CHANGE] Tournament saved successfully") return jsonify({'status': 'success', 'tournament': tournament_state}) else: print(f"[ROUND CHANGE] Error: Failed to save tournament") return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500 except Exception as e: print(f"[ROUND CHANGE] Exception: {str(e)}") import traceback traceback.print_exc() 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/', 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 archive_filename = None if not league_state: # Only archive standalone tournaments archive_success, archive_filename = 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 league_archive_filename = None if league_finished and league_state: league_archive_success, league_archive_filename = 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) # Delete results file only if tournament was archived # For non-final league tournaments, keep results file so /results route can show both league standings and tournament results if archive_success or league_archive_success: # Tournament or league was archived, safe to delete results if os.path.exists(RESULTS_FILE): os.remove(RESULTS_FILE) # If not archived (non-final league tournament), keep results file for display response_data = { 'status': 'success', 'results': results, 'archived': archive_success, 'archive_filename': archive_filename, 'league_archived': league_archive_success if league_finished else None, 'league_archive_filename': league_archive_filename 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) # Unpack tuple but ignore values if league_state: _, _ = archive_league(league_state) # Unpack tuple but ignore values # 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//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//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/') 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/') 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/', 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)