Refactor change...

This commit is contained in:
2025-11-09 12:55:10 +01:00
parent ca1ea820a2
commit ba7e1626e3
42 changed files with 937 additions and 59638 deletions
+6
View File
@@ -0,0 +1,6 @@
"""
TV_APP V1.0.0 - App Package
This package contains support modules for the main Flask application.
All routes are in tv_app.py in the project root.
"""
+13
View File
@@ -0,0 +1,13 @@
"""
Configuration for TV_APP V1.0.0
"""
import os
class Config:
"""Base configuration class"""
SECRET_KEY = os.getenv('SECRET_KEY', 'your-secret-key-for-sessions')
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
HOST = os.getenv('HOST', '0.0.0.0')
PORT = int(os.getenv('PORT', 5000))
+280
View File
@@ -0,0 +1,280 @@
"""
Data models and business logic for TV_APP
Handles all calculations and data structure creation
"""
from datetime import datetime
import random
NUM_CAMERAS = 6
class Tournament:
"""Tournament creation and management"""
@staticmethod
def create_league(enabled_players, tournament_type):
"""Create a new league with 5 tournaments"""
league_id = f"league_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
league_data = {
'league_id': league_id,
'created_at': datetime.now().isoformat(),
'tournament_type': tournament_type,
'total_tournaments': 5,
'current_tournament': 0,
'participants': {},
'completed_tournaments': [],
'league_finished': False
}
# Initialize participants
for player in enabled_players:
league_data['participants'][str(player['id'])] = {
'name': player['name'],
'joker_used': False,
'tournament_results': [],
'total_score': 0,
'final_score': 0,
'tournaments_participated': 0
}
return league_data
@staticmethod
def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None):
"""Create draft groups of 6 players and organize rounds"""
if len(enabled_players) < 1:
return None
# Shuffle players for random grouping
players_copy = enabled_players.copy()
random.shuffle(players_copy)
# Create groups of up to 6
groups = []
for i in range(0, len(players_copy), NUM_CAMERAS):
group = players_copy[i:i + NUM_CAMERAS]
groups.append(group)
# Create rounds
rounds = []
for i, group in enumerate(groups):
rounds.append({
'round_number': i + 1,
'players': group,
'status': 'pending' if i == 0 else 'waiting'
})
tournament_data = {
'rounds': rounds,
'created_at': datetime.now().isoformat(),
'total_players': len(enabled_players),
'total_rounds': len(rounds),
'current_round': 1,
'tournament_type': tournament_type
}
if league_tournament_number:
tournament_data['league_tournament_number'] = league_tournament_number
return tournament_data
@staticmethod
def create_results_structure(tournament_data):
"""Create results structure for a tournament"""
if not tournament_data:
return None
tournament_type = tournament_data.get('tournament_type', '20_targets')
# Determine target count and shots per target
if tournament_type == '40_targets':
num_targets = 40
shots_per_target = 2
elif tournament_type == '4_targets':
num_targets = 4
shots_per_target = 5
else: # 20_targets (default)
num_targets = 20
shots_per_target = 2
results = {
'tournament_id': tournament_data.get('created_at', datetime.now().isoformat()),
'tournament_type': tournament_type,
'participants': {},
'tournament_finished': False,
'created_at': datetime.now().isoformat()
}
# Add league info if present
if 'league_tournament_number' in tournament_data:
results['league_tournament_number'] = tournament_data['league_tournament_number']
# Create structure for each participant
all_players = []
for round_data in tournament_data['rounds']:
all_players.extend(round_data['players'])
for player in all_players:
player_id = str(player['id'])
# Create target structure based on tournament type
targets = {}
for i in range(1, num_targets + 1):
target = {}
for j in range(1, shots_per_target + 1):
target[f'shot{j}'] = None
targets[str(i)] = target
results['participants'][player_id] = {
'name': player['name'],
'targets': targets,
'total_score': 0,
'completed': False
}
return results
class Scoring:
"""Score calculation and ranking"""
@staticmethod
def calculate_total_score(targets):
"""Calculate total score from targets, treating None as 0"""
total = 0
for target in targets.values():
for shot_key, shot_value in target.items():
if shot_key.startswith('shot') and shot_value is not None:
total += shot_value
return total
@staticmethod
def is_participant_completed(targets):
"""Check if a participant has completed all targets"""
for target in targets.values():
for shot_key, shot_value in target.items():
if shot_key.startswith('shot') and shot_value is None:
return False
return True
@staticmethod
def calculate_league_final_scores(league_data):
"""Calculate final league scores using best 4 tournaments"""
for participant_id, participant in league_data['participants'].items():
tournament_scores = []
# Get all tournament scores where player participated
for result in participant['tournament_results']:
if result['participated']:
tournament_scores.append(result['score'])
# Sort scores descending and take best 4
tournament_scores.sort(reverse=True)
best_scores = tournament_scores[:4] if len(tournament_scores) > 4 else tournament_scores
participant['final_score'] = sum(best_scores)
participant['tournaments_participated'] = len(tournament_scores)
@staticmethod
def get_league_final_rankings(league_data):
"""Get final league rankings sorted by final score"""
participants = []
for player_id, data in league_data['participants'].items():
# Calculate total 10s across all tournaments
total_tens = sum(
result.get('tens_count', 0) for result in data['tournament_results']
if result.get('participated', False)
)
participants.append({
'id': player_id,
'name': data['name'],
'final_score': data['final_score'],
'total_score': data['total_score'],
'tournaments_participated': data['tournaments_participated'],
'joker_used': data['joker_used'],
'tournament_results': data['tournament_results'],
'total_tens': total_tens
})
# Sort by final score (best 4 tournaments) descending, then by total 10s
participants.sort(key=lambda x: (x['final_score'], x['total_tens']), reverse=True)
# Add rankings
for i, participant in enumerate(participants):
participant['rank'] = i + 1
return participants
@staticmethod
def calculate_current_league_standings(league_data):
"""Calculate current league standings during active league"""
participants = []
for player_id, data in league_data['participants'].items():
tournament_scores = []
completed_tournaments = 0
total_tens = 0
for result in data['tournament_results']:
if result['participated']:
tournament_scores.append(result['score'])
completed_tournaments += 1
total_tens += result.get('tens_count', 0)
# Current score is sum of all completed tournaments
current_total = sum(tournament_scores)
# For display, show what the final score would be if we took best 4 now
tournament_scores.sort(reverse=True)
projected_final = sum(tournament_scores[:4]) if len(tournament_scores) >= 4 else sum(tournament_scores)
participants.append({
'id': player_id,
'name': data['name'],
'current_total': current_total,
'projected_final': projected_final,
'tournaments_completed': completed_tournaments,
'joker_used': data['joker_used'],
'tournament_results': data['tournament_results'],
'total_tens': total_tens
})
# Sort by current total score (descending), then by total 10s
participants.sort(key=lambda x: (x['current_total'], x['total_tens']), reverse=True)
# Add rankings
for i, participant in enumerate(participants):
participant['rank'] = i + 1
return participants
class RoundManager:
"""Tournament round management"""
@staticmethod
def get_current_round_data(tournament_state):
"""Get current round data from tournament"""
if not tournament_state:
return None
current_round_num = tournament_state.get('current_round', 1)
# Find the current round
for round_data in tournament_state['rounds']:
if round_data['round_number'] == current_round_num:
return round_data
return None
@staticmethod
def get_tournament_format_description(tournament_type):
"""Get tournament format description"""
formats = {
'20_targets': '20 Targets, 2 Shots Per Target',
'40_targets': '40 Targets, 2 Shots Per Target',
'4_targets': '4 Targets, 5 Shots Per Target'
}
return formats.get(tournament_type, 'Unknown Format')
+312
View File
@@ -0,0 +1,312 @@
"""
File I/O and data persistence for TV_APP
Handles all JSON file operations and data archiving
"""
import json
import os
import glob
from datetime import datetime
# File paths - organized in data directory
SETTINGS_FILE = 'data/camera_settings.json'
PLAYERS_FILE = 'data/players.json'
TOURNAMENT_FILE = 'data/tournament_state.json'
RESULTS_FILE = 'data/tournament_results.json'
LEAGUE_FILE = 'data/league_state.json'
ARCHIVE_DIR = 'data/tournament_archives'
LEAGUE_ARCHIVE_DIR = 'data/league_archives'
# Default settings
DEFAULT_SETTINGS = {
'camera_titles': {
'1': 'Camera 1',
'2': 'Camera 2',
'3': 'Camera 3',
'4': 'Camera 4',
'5': 'Camera 5',
'6': 'Camera 6'
},
'display_options': {
'show_titles': True,
'title_size': 1.1
}
}
class FileStorage:
"""Handle JSON file read/write operations"""
@staticmethod
def _ensure_directory(directory):
"""Ensure directory exists"""
if not os.path.exists(directory):
os.makedirs(directory)
@staticmethod
def _read_json(filepath):
"""Read JSON file safely"""
try:
if os.path.exists(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(f"Error reading {filepath}: {e}")
return None
@staticmethod
def _write_json(filepath, data):
"""Write JSON file safely"""
try:
directory = os.path.dirname(filepath)
FileStorage._ensure_directory(directory)
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
return True
except IOError as e:
print(f"Error writing {filepath}: {e}")
return False
class SettingsStorage(FileStorage):
"""Manage camera and display settings"""
@staticmethod
def load_settings():
"""Load settings from JSON file, create with defaults if not exists"""
settings = FileStorage._read_json(SETTINGS_FILE)
if settings is None:
return DEFAULT_SETTINGS.copy()
# Ensure all required keys exist (backwards compatibility)
for key in DEFAULT_SETTINGS:
if key not in settings:
settings[key] = DEFAULT_SETTINGS[key]
for camera_id in DEFAULT_SETTINGS['camera_titles']:
if camera_id not in settings.get('camera_titles', {}):
settings['camera_titles'][camera_id] = DEFAULT_SETTINGS['camera_titles'][camera_id]
return settings
@staticmethod
def save_settings(settings):
"""Save settings to JSON file"""
return FileStorage._write_json(SETTINGS_FILE, settings)
class PlayerStorage(FileStorage):
"""Manage player data"""
DEFAULT_PLAYERS = {
'players': [
{'id': i, 'name': f'Player {i}', 'enabled': True}
for i in range(1, 7) # 6 cameras
]
}
@staticmethod
def load_players():
"""Load players from JSON file, create with defaults if not exists"""
players = FileStorage._read_json(PLAYERS_FILE)
if players is None:
PlayerStorage.save_players(PlayerStorage.DEFAULT_PLAYERS)
return PlayerStorage.DEFAULT_PLAYERS.copy()
return players
@staticmethod
def save_players(players_data):
"""Save players to JSON file"""
return FileStorage._write_json(PLAYERS_FILE, players_data)
class TournamentStorage(FileStorage):
"""Manage tournament state"""
@staticmethod
def load_tournament_state():
"""Load tournament state from JSON file"""
return FileStorage._read_json(TOURNAMENT_FILE)
@staticmethod
def save_tournament_state(tournament_data):
"""Save tournament state to JSON file"""
return FileStorage._write_json(TOURNAMENT_FILE, tournament_data)
class ResultsStorage(FileStorage):
"""Manage tournament results"""
@staticmethod
def load_results():
"""Load results from JSON file"""
return FileStorage._read_json(RESULTS_FILE)
@staticmethod
def save_results(results_data):
"""Save results to JSON file"""
return FileStorage._write_json(RESULTS_FILE, results_data)
class LeagueStorage(FileStorage):
"""Manage league state"""
@staticmethod
def load_league_state():
"""Load league state from JSON file"""
return FileStorage._read_json(LEAGUE_FILE)
@staticmethod
def save_league_state(league_data):
"""Save league state to JSON file"""
return FileStorage._write_json(LEAGUE_FILE, league_data)
class ArchiveStorage(FileStorage):
"""Manage tournament and league archives"""
@staticmethod
def archive_tournament(tournament_data, results_data):
"""Archive completed tournament data"""
try:
FileStorage._ensure_directory(ARCHIVE_DIR)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_filename = f"tournament_{timestamp}.json"
archive_path = os.path.join(ARCHIVE_DIR, archive_filename)
archive_data = {
'tournament': tournament_data,
'results': results_data,
'archived_at': datetime.now().isoformat()
}
success = FileStorage._write_json(archive_path, archive_data)
if success:
print(f"Tournament archived to: {archive_path}")
return success
except Exception as e:
print(f"Error archiving tournament: {e}")
return False
@staticmethod
def archive_league(league_data):
"""Archive completed league data"""
try:
FileStorage._ensure_directory(LEAGUE_ARCHIVE_DIR)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_filename = f"league_{timestamp}.json"
archive_path = os.path.join(LEAGUE_ARCHIVE_DIR, archive_filename)
archive_data = {
'league': league_data,
'archived_at': datetime.now().isoformat()
}
success = FileStorage._write_json(archive_path, archive_data)
if success:
print(f"League archived to: {archive_path}")
return success
except Exception as e:
print(f"Error archiving league: {e}")
return False
@staticmethod
def get_archived_tournaments():
"""Get list of standalone archived tournaments"""
try:
if not os.path.exists(ARCHIVE_DIR):
return []
archives = []
for file_path in glob.glob(os.path.join(ARCHIVE_DIR, "tournament_*.json")):
try:
data = FileStorage._read_json(file_path)
if not data:
continue
filename = os.path.basename(file_path)
archived_at = data.get('archived_at', 'Unknown')
tournament_data = data.get('tournament', {})
results_data = data.get('results', {})
# Skip tournaments that are part of a league
if tournament_data.get('league_tournament_number') or results_data.get('league_tournament_number'):
continue
archive_info = {
'filename': filename,
'filepath': file_path,
'archived_at': archived_at,
'created_at': tournament_data.get('created_at', 'Unknown'),
'tournament_type': tournament_data.get('tournament_type', '20_targets'),
'total_players': tournament_data.get('total_players', 0),
'total_rounds': tournament_data.get('total_rounds', 0),
'tournament_finished': results_data.get('tournament_finished', False),
'participants_count': len(results_data.get('participants', {}))
}
archives.append(archive_info)
except (json.JSONDecodeError, IOError) as e:
print(f"Error reading archive {file_path}: {e}")
continue
# Sort by archived date (newest first)
archives.sort(key=lambda x: x['archived_at'], reverse=True)
return archives
except Exception as e:
print(f"Error getting archived tournaments: {e}")
return []
@staticmethod
def get_archived_leagues():
"""Get list of all archived leagues"""
try:
if not os.path.exists(LEAGUE_ARCHIVE_DIR):
return []
archives = []
for file_path in glob.glob(os.path.join(LEAGUE_ARCHIVE_DIR, "league_*.json")):
try:
data = FileStorage._read_json(file_path)
if not data:
continue
filename = os.path.basename(file_path)
archived_at = data.get('archived_at', 'Unknown')
league_data = data.get('league', {})
archive_info = {
'filename': filename,
'filepath': file_path,
'archived_at': archived_at,
'created_at': league_data.get('created_at', 'Unknown'),
'league_id': league_data.get('league_id', 'Unknown'),
'tournament_type': league_data.get('tournament_type', '20_targets'),
'total_tournaments': league_data.get('total_tournaments', 5),
'participants_count': len(league_data.get('participants', {})),
'league_finished': league_data.get('league_finished', False),
'completed_tournaments': len(league_data.get('completed_tournaments', []))
}
archives.append(archive_info)
except (json.JSONDecodeError, IOError) as e:
print(f"Error reading league archive {file_path}: {e}")
continue
# Sort by archived date (newest first)
archives.sort(key=lambda x: x['archived_at'], reverse=True)
return archives
except Exception as e:
print(f"Error getting archived leagues: {e}")
return []
@staticmethod
def load_archive_file(filepath):
"""Load a specific archive file"""
return FileStorage._read_json(filepath)
+78
View File
@@ -0,0 +1,78 @@
"""
Utility functions for TV_APP
Helper functions for translations, device detection, and common calculations
"""
import json
import os
import re
from flask import request, session
DEFAULT_LANGUAGE = 'sl'
def load_translations(language='sl'):
"""Load translations for the specified language"""
try:
locale_file = os.path.join('locales', f'{language}.json')
if os.path.exists(locale_file):
with open(locale_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading translations for {language}: {e}")
# Fallback to default language
try:
default_file = os.path.join('locales', f'{DEFAULT_LANGUAGE}.json')
with open(default_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Error loading default translations: {e}")
return {}
def get_current_language():
"""Get current language from session or default"""
return session.get('language', DEFAULT_LANGUAGE)
def get_translations():
"""Get translations for current language"""
return load_translations(get_current_language())
def is_mobile_device():
"""Check if the request is coming from a mobile device"""
user_agent = request.headers.get('User-Agent', '').lower()
mobile_patterns = [
r'android', r'iphone', r'ipad', r'ipod', r'blackberry',
r'iemobile', r'opera mini', r'mobile', r'tablet'
]
return any(re.search(pattern, user_agent) for pattern in mobile_patterns)
def calculate_tens_from_targets(targets):
"""Calculate the number of 10s from a targets dictionary"""
tens_count = 0
if not targets:
return 0
for target in targets.values():
if isinstance(target, dict):
for shot_key, shot_value in target.items():
if shot_key.startswith('shot') and shot_value == 10:
tens_count += 1
return tens_count
def validate_player_data(player_data):
"""Validate player data structure"""
required_fields = ['id', 'name', 'enabled']
return all(field in player_data for field in required_fields)
def validate_settings(settings):
"""Validate settings structure"""
required_sections = ['camera_titles', 'display_options']
return all(section in settings for section in required_sections)