2343 lines
92 KiB
Python
2343 lines
92 KiB
Python
"""
|
||
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/<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:
|
||
# 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/<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) # 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/<int:round_number>', 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/<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
|
||
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/<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) |