Almost there!

parent e8a91c81
...@@ -119,6 +119,7 @@ class GamesThread(ThreadedComponent): ...@@ -119,6 +119,7 @@ class GamesThread(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message) self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message)
self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_match_start) self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_match_start)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH_DONE, self._handle_play_video_match_done) self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH_DONE, self._handle_play_video_match_done)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_RESULT_DONE, self._handle_play_video_result_done)
self.message_bus.subscribe(self.name, MessageType.MATCH_DONE, self._handle_match_done) self.message_bus.subscribe(self.name, MessageType.MATCH_DONE, self._handle_match_done)
self.message_bus.subscribe(self.name, MessageType.GAME_STATUS, self._handle_game_status_request) self.message_bus.subscribe(self.name, MessageType.GAME_STATUS, self._handle_game_status_request)
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status) self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
...@@ -148,8 +149,8 @@ class GamesThread(ThreadedComponent): ...@@ -148,8 +149,8 @@ class GamesThread(ThreadedComponent):
try: try:
while self.running and not self._shutdown_event.is_set(): while self.running and not self._shutdown_event.is_set():
try: try:
# Process any pending messages # Process any pending messages with shorter timeout for responsive shutdown
message = self.message_bus.get_message(self.name, timeout=0.1) message = self.message_bus.get_message(self.name, timeout=0.05)
if message: if message:
self._process_message(message) self._process_message(message)
...@@ -160,12 +161,16 @@ class GamesThread(ThreadedComponent): ...@@ -160,12 +161,16 @@ class GamesThread(ThreadedComponent):
# Update heartbeat # Update heartbeat
self.heartbeat() self.heartbeat()
# Sleep for 0.1 seconds as requested # Check shutdown event more frequently
time.sleep(0.1) if self._shutdown_event.wait(0.05):
break
except Exception as e: except Exception as e:
logger.error(f"GamesThread run loop error: {e}") logger.error(f"GamesThread run loop error: {e}")
time.sleep(1.0) # Longer sleep on error # Shorter sleep on error to be more responsive to shutdown
if not self._shutdown_event.wait(0.5):
continue
break
except Exception as e: except Exception as e:
logger.error(f"GamesThread run failed: {e}") logger.error(f"GamesThread run failed: {e}")
...@@ -351,6 +356,14 @@ class GamesThread(ThreadedComponent): ...@@ -351,6 +356,14 @@ class GamesThread(ThreadedComponent):
# Send PLAY_VIDEO_MATCH with calculated result # Send PLAY_VIDEO_MATCH with calculated result
self._send_play_video_match(fixture_id, match_id, result) self._send_play_video_match(fixture_id, match_id, result)
# Set match status to 'ingame' before performing extraction
self._set_match_status(match_id, 'ingame')
# Perform result extraction immediately after sending PLAY_VIDEO_MATCH
logger.info(f"🔍 [EXTRACTION DEBUG] Starting immediate extraction after PLAY_VIDEO_MATCH for fixture {fixture_id}, match {match_id}")
extracted_result = self._perform_result_extraction(fixture_id, match_id)
logger.info(f"✅ [EXTRACTION DEBUG] Extraction completed immediately: {extracted_result}")
except Exception as e: except Exception as e:
logger.error(f"Failed to handle MATCH_START message: {e}") logger.error(f"Failed to handle MATCH_START message: {e}")
# Fallback to random selection if betting calculation fails # Fallback to random selection if betting calculation fails
...@@ -430,6 +443,8 @@ class GamesThread(ThreadedComponent): ...@@ -430,6 +443,8 @@ class GamesThread(ThreadedComponent):
self._handle_game_update(message) self._handle_game_update(message)
elif message.type == MessageType.PLAY_VIDEO_MATCH_DONE: elif message.type == MessageType.PLAY_VIDEO_MATCH_DONE:
self._handle_play_video_match_done(message) self._handle_play_video_match_done(message)
elif message.type == MessageType.PLAY_VIDEO_RESULT_DONE:
self._handle_play_video_result_done(message)
elif message.type == MessageType.MATCH_DONE: elif message.type == MessageType.MATCH_DONE:
self._handle_match_done(message) self._handle_match_done(message)
elif message.type == MessageType.MATCH_START: elif message.type == MessageType.MATCH_START:
...@@ -1178,6 +1193,18 @@ class GamesThread(ThreadedComponent): ...@@ -1178,6 +1193,18 @@ class GamesThread(ThreadedComponent):
try: try:
from .message_bus import MessageBuilder from .message_bus import MessageBuilder
# Check if there are at least 5 matches remaining in the fixture
logger.info(f"🎬 Checking minimum match count for fixture {fixture_id} before playing INTRO.mp4")
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
logger.info(f"🎬 Fixture {fixture_id} has {remaining_matches} remaining matches")
if remaining_matches < 5:
logger.info(f"🚨 Only {remaining_matches} matches remaining (minimum 5 required) - creating new matches")
self._ensure_minimum_matches_in_fixture(fixture_id, 5 - remaining_matches)
# Recount after adding matches
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
logger.info(f"✅ After adding matches, fixture {fixture_id} now has {remaining_matches} remaining matches")
# Find the first match that was set to 'bet' status # Find the first match that was set to 'bet' status
first_bet_match_id = self._get_first_bet_match_id(fixture_id) first_bet_match_id = self._get_first_bet_match_id(fixture_id)
...@@ -1192,10 +1219,10 @@ class GamesThread(ThreadedComponent): ...@@ -1192,10 +1219,10 @@ class GamesThread(ThreadedComponent):
) )
self.message_bus.publish(start_intro_message, broadcast=True) self.message_bus.publish(start_intro_message, broadcast=True)
logger.info(f"START_INTRO message dispatched for fixture {fixture_id}, match {first_bet_match_id}") logger.info(f"🎬 START_INTRO message dispatched for fixture {fixture_id}, match {first_bet_match_id}")
except Exception as e: except Exception as e:
logger.error(f"Failed to dispatch START_INTRO message: {e}") logger.error(f"Failed to dispatch START_INTRO message: {e}")
def _get_first_bet_match_id(self, fixture_id: str) -> Optional[int]: def _get_first_bet_match_id(self, fixture_id: str) -> Optional[int]:
"""Get the ID of the first match set to 'bet' status in the fixture""" """Get the ID of the first match set to 'bet' status in the fixture"""
...@@ -1234,6 +1261,9 @@ class GamesThread(ThreadedComponent): ...@@ -1234,6 +1261,9 @@ class GamesThread(ThreadedComponent):
logger.info(f"DEBUG: Starting ZIP extraction for match {match_id}") logger.info(f"DEBUG: Starting ZIP extraction for match {match_id}")
# CLEANUP: Delete all previous unzipped match directories before proceeding
self._cleanup_previous_match_extractions()
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
# Get the match from database # Get the match from database
...@@ -1587,7 +1617,7 @@ class GamesThread(ThreadedComponent): ...@@ -1587,7 +1617,7 @@ class GamesThread(ThreadedComponent):
return f"{result}.mp4" # Fallback return f"{result}.mp4" # Fallback
def _handle_play_video_match_done(self, message: Message): def _handle_play_video_match_done(self, message: Message):
"""Handle PLAY_VIDEO_MATCH_DONE message and perform result extraction""" """Handle PLAY_VIDEO_MATCH_DONE message and query database for result"""
try: try:
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id") match_id = message.data.get("match_id")
...@@ -1597,25 +1627,55 @@ class GamesThread(ThreadedComponent): ...@@ -1597,25 +1627,55 @@ class GamesThread(ThreadedComponent):
# Set match status to 'ingame' # Set match status to 'ingame'
self._set_match_status(match_id, 'ingame') self._set_match_status(match_id, 'ingame')
# Perform result extraction # Query database for the previously extracted result
extracted_result = self._perform_result_extraction(fixture_id, match_id) extracted_result = self._query_extracted_result(match_id)
# Send PLAY_VIDEO_RESULT message if extracted_result:
self._send_play_video_result(fixture_id, match_id, extracted_result) logger.info(f"Found extracted result for match {match_id}: {extracted_result}")
# Send PLAY_VIDEO_RESULTS message (note: RESULTS plural as per user request)
self._send_play_video_results(fixture_id, match_id, extracted_result)
else:
logger.error(f"No extracted result found for match {match_id}")
# Fallback to random selection
fallback_result = self._fallback_result_selection()
logger.info(f"Using fallback result for match {match_id}: {fallback_result}")
self._send_play_video_results(fixture_id, match_id, fallback_result)
except Exception as e: except Exception as e:
logger.error(f"Failed to handle PLAY_VIDEO_MATCH_DONE message: {e}") logger.error(f"Failed to handle PLAY_VIDEO_MATCH_DONE message: {e}")
def _handle_play_video_result_done(self, message: Message):
"""Handle PLAY_VIDEO_RESULTS_DONE message - result video finished, send MATCH_DONE and START_INTRO"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
result = message.data.get("result")
logger.info(f"Processing PLAY_VIDEO_RESULTS_DONE for fixture {fixture_id}, match {match_id}, result {result}")
# Update match status to 'done' and save result
self._set_match_status_and_result(match_id, 'done', result)
# Send MATCH_DONE message with result
self._send_match_done(fixture_id, match_id, result)
# Send START_INTRO message to start the next match cycle
self._dispatch_start_intro(fixture_id)
except Exception as e:
logger.error(f"Failed to handle PLAY_VIDEO_RESULTS_DONE message: {e}")
def _handle_match_done(self, message: Message): def _handle_match_done(self, message: Message):
"""Handle MATCH_DONE message""" """Handle MATCH_DONE message"""
try: try:
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id") match_id = message.data.get("match_id")
result = message.data.get("result")
logger.info(f"Processing MATCH_DONE for fixture {fixture_id}, match {match_id}") logger.info(f"Processing MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}")
# Update match status to 'done' # Update match status to 'done' and save result
self._set_match_status(match_id, 'done') self._set_match_status_and_result(match_id, 'done', result)
# Wait 2 seconds then send NEXT_MATCH # Wait 2 seconds then send NEXT_MATCH
import time import time
...@@ -1709,38 +1769,73 @@ class GamesThread(ThreadedComponent): ...@@ -1709,38 +1769,73 @@ class GamesThread(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to set match status: {e}") logger.error(f"Failed to set match status: {e}")
def _set_match_status_and_result(self, match_id: int, status: str, result: str):
"""Set match status and result in database"""
try:
session = self.db_manager.get_session()
try:
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
match.status = status
match.result = result
session.commit()
logger.info(f"Updated match {match_id} status to {status} and result to {result}")
else:
logger.error(f"Match {match_id} not found")
finally:
session.close()
except Exception as e:
logger.error(f"Failed to set match status and result: {e}")
def _perform_result_extraction(self, fixture_id: str, match_id: int) -> str: def _perform_result_extraction(self, fixture_id: str, match_id: int) -> str:
"""Perform complex result extraction logic""" """Perform complex result extraction logic"""
try: try:
logger.info(f"🔍 [EXTRACTION DEBUG] Starting result extraction for fixture {fixture_id}, match {match_id}")
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
# Get all result options (excluding UNDER/OVER) # Step 1: Get match outcomes to determine available result options
logger.info(f"📊 [EXTRACTION DEBUG] Step 1: Retrieving match outcomes for match {match_id}")
match_outcomes = session.query(MatchOutcomeModel).filter(
MatchOutcomeModel.match_id == match_id
).all()
available_outcome_names = [outcome.column_name for outcome in match_outcomes]
logger.info(f"📊 [EXTRACTION DEBUG] Found {len(match_outcomes)} match outcomes: {available_outcome_names}")
# Step 2: Get result options that correspond to match outcomes (excluding UNDER/OVER)
logger.info(f"🎯 [EXTRACTION DEBUG] Step 2: Filtering result options to match fixture outcomes")
from ..database.models import ResultOptionModel, AvailableBetModel, ExtractionAssociationModel from ..database.models import ResultOptionModel, AvailableBetModel, ExtractionAssociationModel
# Only include result options that have corresponding outcomes in this match
result_options = session.query(ResultOptionModel).filter( result_options = session.query(ResultOptionModel).filter(
ResultOptionModel.is_active == True ResultOptionModel.is_active == True,
ResultOptionModel.result_name.in_(available_outcome_names),
~ResultOptionModel.result_name.in_(['UNDER', 'OVER']) # Exclude UNDER/OVER as handled separately
).all() ).all()
logger.info(f"🎯 [EXTRACTION DEBUG] Filtered to {len(result_options)} result options: {[opt.result_name for opt in result_options]}")
payouts = {} payouts = {}
total_bet_amount = 0.0 total_bet_amount = 0.0
# For each result option, calculate payout # Step 3: Calculate payouts for each result option
logger.info(f"💰 [EXTRACTION DEBUG] Step 3: Calculating payouts for {len(result_options)} result options")
for result_option in result_options: for result_option in result_options:
result_name = result_option.result_name result_name = result_option.result_name
logger.info(f"💰 [EXTRACTION DEBUG] Processing result option: {result_name}")
# Skip UNDER and OVER as they are handled separately
if result_name in ['UNDER', 'OVER']:
continue
# Get associated available bets for this result # Get associated available bets for this result
associations = session.query(ExtractionAssociationModel).filter( associations = session.query(ExtractionAssociationModel).filter(
ExtractionAssociationModel.extraction_result == result_name ExtractionAssociationModel.extraction_result == result_name
).all() ).all()
logger.info(f"💰 [EXTRACTION DEBUG] Found {len(associations)} associations for {result_name}")
payout = 0.0 payout = 0.0
for association in associations: for association in associations:
outcome_name = association.outcome_name outcome_name = association.outcome_name
logger.info(f"💰 [EXTRACTION DEBUG] Processing association: {outcome_name} -> {result_name}")
# Get coefficient for this outcome from match outcomes # Get coefficient for this outcome from match outcomes
match_outcome = session.query(MatchOutcomeModel).filter( match_outcome = session.query(MatchOutcomeModel).filter(
...@@ -1750,6 +1845,7 @@ class GamesThread(ThreadedComponent): ...@@ -1750,6 +1845,7 @@ class GamesThread(ThreadedComponent):
if match_outcome: if match_outcome:
coefficient = match_outcome.float_value coefficient = match_outcome.float_value
logger.info(f"💰 [EXTRACTION DEBUG] Found coefficient for {outcome_name}: {coefficient}")
# Get total bets for this outcome on this match # Get total bets for this outcome on this match
bet_amount = session.query(BetDetailModel.amount).filter( bet_amount = session.query(BetDetailModel.amount).filter(
...@@ -1759,11 +1855,17 @@ class GamesThread(ThreadedComponent): ...@@ -1759,11 +1855,17 @@ class GamesThread(ThreadedComponent):
).all() ).all()
total_outcome_amount = sum(bet.amount for bet in bet_amount) if bet_amount else 0.0 total_outcome_amount = sum(bet.amount for bet in bet_amount) if bet_amount else 0.0
logger.info(f"💰 [EXTRACTION DEBUG] Total bet amount for {outcome_name}: {total_outcome_amount:.2f}")
payout += total_outcome_amount * coefficient payout += total_outcome_amount * coefficient
logger.info(f"💰 [EXTRACTION DEBUG] Added payout for {outcome_name}: {total_outcome_amount:.2f} × {coefficient} = {total_outcome_amount * coefficient:.2f}")
else:
logger.warning(f"💰 [EXTRACTION DEBUG] No match outcome found for {outcome_name}")
payouts[result_name] = payout payouts[result_name] = payout
logger.info(f"💰 [EXTRACTION DEBUG] Total payout for {result_name}: {payout:.2f}")
# Calculate total bet amount (excluding UNDER/OVER) # Step 4: Calculate total bet amount (excluding UNDER/OVER)
logger.info(f"💵 [EXTRACTION DEBUG] Step 4: Calculating total bet amount for match {match_id}")
all_bets = session.query(BetDetailModel).filter( all_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending', BetDetailModel.result == 'pending',
...@@ -1771,39 +1873,53 @@ class GamesThread(ThreadedComponent): ...@@ -1771,39 +1873,53 @@ class GamesThread(ThreadedComponent):
).all() ).all()
total_bet_amount = sum(bet.amount for bet in all_bets) if all_bets else 0.0 total_bet_amount = sum(bet.amount for bet in all_bets) if all_bets else 0.0
logger.info(f"💵 [EXTRACTION DEBUG] Total bet amount calculated: {total_bet_amount:.2f} from {len(all_bets)} bets")
# Get redistribution CAP # Step 5: Get redistribution CAP
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Retrieving redistribution CAP")
cap_percentage = self._get_redistribution_cap() cap_percentage = self._get_redistribution_cap()
cap_threshold = total_bet_amount * (cap_percentage / 100.0) cap_threshold = total_bet_amount * (cap_percentage / 100.0)
logger.info(f"🎯 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, threshold: {cap_threshold:.2f}")
logger.info(f"Result extraction for match {match_id}: {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}") logger.info(f"📊 [EXTRACTION DEBUG] Extraction summary - {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Payouts: {payouts}")
# Filter payouts below CAP threshold # Step 6: Filter payouts below CAP threshold
logger.info(f"🎯 [EXTRACTION DEBUG] Step 6: Filtering payouts below CAP threshold")
eligible_payouts = {k: v for k, v in payouts.items() if v <= cap_threshold} eligible_payouts = {k: v for k, v in payouts.items() if v <= cap_threshold}
logger.info(f"🎯 [EXTRACTION DEBUG] Eligible payouts (≤ {cap_threshold:.2f}): {eligible_payouts}")
if not eligible_payouts: if not eligible_payouts:
# No payouts below CAP, select the lowest payout # No payouts below CAP, select the lowest payout
eligible_payouts = {min(payouts, key=payouts.get): payouts[min(payouts, key=payouts.get)]} lowest_payout_result = min(payouts, key=payouts.get)
logger.info("No payouts below CAP, selecting lowest payout") eligible_payouts = {lowest_payout_result: payouts[lowest_payout_result]}
logger.info(f"🚨 [EXTRACTION DEBUG] No payouts below CAP, selecting lowest payout: {lowest_payout_result} ({payouts[lowest_payout_result]:.2f})")
# Perform weighted random selection based on coefficients # Step 7: Perform weighted random selection
logger.info(f"🎲 [EXTRACTION DEBUG] Step 7: Performing weighted random selection from {len(eligible_payouts)} eligible results")
selected_result = self._weighted_result_selection(eligible_payouts, session, match_id) selected_result = self._weighted_result_selection(eligible_payouts, session, match_id)
logger.info(f"🎯 [EXTRACTION DEBUG] Selected result: {selected_result}")
# Update bet results for UNDER/OVER and the selected result # Step 8: Update bet results
logger.info(f"💾 [EXTRACTION DEBUG] Step 8: Updating bet results for match {match_id}")
self._update_bet_results(match_id, selected_result, session) self._update_bet_results(match_id, selected_result, session)
# Collect statistics for this match completion # Step 9: Collect statistics
logger.info(f"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics")
self._collect_match_statistics(match_id, fixture_id, selected_result, session) self._collect_match_statistics(match_id, fixture_id, selected_result, session)
logger.info(f"Result extraction completed: selected {selected_result}") logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}")
return selected_result return selected_result
finally: finally:
session.close() session.close()
except Exception as e: except Exception as e:
logger.error(f"Failed to perform result extraction: {e}") logger.error(f"❌ [EXTRACTION DEBUG] Failed to perform result extraction: {e}")
import traceback
logger.error(f"❌ [EXTRACTION DEBUG] Full traceback: {traceback.format_exc()}")
# Fallback to random selection # Fallback to random selection
logger.info(f"🔄 [EXTRACTION DEBUG] Using fallback random selection")
return self._fallback_result_selection() return self._fallback_result_selection()
def _weighted_result_selection(self, eligible_payouts: Dict[str, float], session, match_id: int) -> str: def _weighted_result_selection(self, eligible_payouts: Dict[str, float], session, match_id: int) -> str:
...@@ -1943,57 +2059,57 @@ class GamesThread(ThreadedComponent): ...@@ -1943,57 +2059,57 @@ class GamesThread(ThreadedComponent):
return return
# Calculate statistics # Calculate statistics
total_bets = session.query(BetDetailModel).filter( total_bets = session.query(BetDetailModel).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.active_status == True MatchModel.active_status == True
).count() ).count()
total_amount_collected = session.query( total_amount_collected = session.query(
BetDetailModel.amount BetDetailModel.amount
).filter( ).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.active_status == True MatchModel.active_status == True
).all() ).all()
total_amount_collected = sum(bet.amount for bet in total_amount_collected) if total_amount_collected else 0.0 total_amount_collected = sum(bet.amount for bet in total_amount_collected) if total_amount_collected else 0.0
# Calculate redistribution amount (sum of all win_amounts) # Calculate redistribution amount (sum of all win_amounts)
total_redistributed = session.query( total_redistributed = session.query(
BetDetailModel.win_amount BetDetailModel.win_amount
).filter( ).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.result == 'win', BetDetailModel.result == 'win',
BetDetailModel.active_status == True MatchModel.active_status == True
).all() ).all()
total_redistributed = sum(bet.win_amount for bet in total_redistributed) if total_redistributed else 0.0 total_redistributed = sum(bet.win_amount for bet in total_redistributed) if total_redistributed else 0.0
# Get UNDER/OVER specific statistics # Get UNDER/OVER specific statistics
under_bets = session.query(BetDetailModel).filter( under_bets = session.query(BetDetailModel).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER', BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True MatchModel.active_status == True
).count() ).count()
under_amount = session.query( under_amount = session.query(
BetDetailModel.amount BetDetailModel.amount
).filter( ).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER', BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True MatchModel.active_status == True
).all() ).all()
under_amount = sum(bet.amount for bet in under_amount) if under_amount else 0.0 under_amount = sum(bet.amount for bet in under_amount) if under_amount else 0.0
over_bets = session.query(BetDetailModel).filter( over_bets = session.query(BetDetailModel).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER', BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True MatchModel.active_status == True
).count() ).count()
over_amount = session.query( over_amount = session.query(
BetDetailModel.amount BetDetailModel.amount
).filter( ).join(MatchModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER', BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True MatchModel.active_status == True
).all() ).all()
over_amount = sum(bet.amount for bet in over_amount) if over_amount else 0.0 over_amount = sum(bet.amount for bet in over_amount) if over_amount else 0.0
...@@ -2068,6 +2184,103 @@ class GamesThread(ThreadedComponent): ...@@ -2068,6 +2184,103 @@ class GamesThread(ThreadedComponent):
logger.error(f"Fallback result selection failed: {e}") logger.error(f"Fallback result selection failed: {e}")
return "WIN1" return "WIN1"
def _query_extracted_result(self, match_id: int) -> Optional[str]:
"""Query database for previously extracted result"""
try:
session = self.db_manager.get_session()
try:
# Query for the most recent extraction stats for this match
from ..database.models import ExtractionStatsModel
extraction_stats = session.query(ExtractionStatsModel).filter(
ExtractionStatsModel.match_id == match_id
).order_by(ExtractionStatsModel.created_at.desc()).first()
if extraction_stats and extraction_stats.actual_result:
logger.info(f"Found extracted result for match {match_id}: {extraction_stats.actual_result}")
return extraction_stats.actual_result
else:
logger.warning(f"No extraction stats found for match {match_id}")
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to query extracted result for match {match_id}: {e}")
return None
def _determine_under_over_result(self, match_id: int, main_result: str) -> Optional[str]:
"""Determine the under/over result for display purposes"""
try:
session = self.db_manager.get_session()
try:
# If the main result is already UNDER or OVER, return it
if main_result in ['UNDER', 'OVER']:
return main_result
# Check if there are winning UNDER or OVER bets for this match
from ..database.models import BetDetailModel
winning_under_over = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'win',
BetDetailModel.outcome.in_(['UNDER', 'OVER'])
).first()
if winning_under_over:
logger.info(f"Found winning {winning_under_over.outcome} bet for match {match_id}")
return winning_under_over.outcome
# If no winning UNDER/OVER bets, check if there were any UNDER/OVER bets at all
# and determine based on some logic (e.g., random or based on main result characteristics)
under_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER'
).count()
over_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER'
).count()
# Simple logic: if there were UNDER bets, show UNDER; if OVER bets, show OVER; otherwise random
if under_bets > 0 and over_bets == 0:
return 'UNDER'
elif over_bets > 0 and under_bets == 0:
return 'OVER'
elif under_bets > 0 and over_bets > 0:
# Both types of bets exist, use simple random selection
import random
return 'UNDER' if random.random() < 0.5 else 'OVER'
else:
# No UNDER/OVER bets, don't show under/over result
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to determine under/over result for match {match_id}: {e}")
return None
def _send_play_video_results(self, fixture_id: str, match_id: int, result: str):
"""Send PLAY_VIDEO_RESULTS message (plural as per user request)"""
try:
# Determine under/over result separately from main result
under_over_result = self._determine_under_over_result(match_id, result)
play_results_message = MessageBuilder.play_video_result(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id,
result=result,
under_over_result=under_over_result
)
self.message_bus.publish(play_results_message)
logger.info(f"Sent PLAY_VIDEO_RESULTS for fixture {fixture_id}, match {match_id}, result {result}, under_over {under_over_result}")
except Exception as e:
logger.error(f"Failed to send PLAY_VIDEO_RESULTS: {e}")
def _send_play_video_result(self, fixture_id: str, match_id: int, result: str): def _send_play_video_result(self, fixture_id: str, match_id: int, result: str):
"""Send PLAY_VIDEO_RESULT message""" """Send PLAY_VIDEO_RESULT message"""
try: try:
...@@ -2083,6 +2296,21 @@ class GamesThread(ThreadedComponent): ...@@ -2083,6 +2296,21 @@ class GamesThread(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to send PLAY_VIDEO_RESULT: {e}") logger.error(f"Failed to send PLAY_VIDEO_RESULT: {e}")
def _send_match_done(self, fixture_id: str, match_id: int, result: str = None):
"""Send MATCH_DONE message"""
try:
match_done_message = MessageBuilder.match_done(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id,
result=result
)
self.message_bus.publish(match_done_message)
logger.info(f"Sent MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}")
except Exception as e:
logger.error(f"Failed to send MATCH_DONE: {e}")
def _send_next_match(self, fixture_id: str, match_id: int): def _send_next_match(self, fixture_id: str, match_id: int):
"""Send NEXT_MATCH message""" """Send NEXT_MATCH message"""
try: try:
...@@ -2306,6 +2534,139 @@ class GamesThread(ThreadedComponent): ...@@ -2306,6 +2534,139 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to determine game status: {e}") logger.error(f"Failed to determine game status: {e}")
return "ready" # Default fallback return "ready" # Default fallback
def _count_remaining_matches_in_fixture(self, fixture_id: str) -> int:
"""Count remaining matches in fixture that can still be played"""
try:
session = self.db_manager.get_session()
try:
# Count matches that are not in terminal states (done, cancelled, failed, paused)
remaining_count = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.status.notin_(['done', 'cancelled', 'failed', 'paused']),
MatchModel.active_status == True
).count()
logger.debug(f"Fixture {fixture_id} has {remaining_count} remaining matches")
return remaining_count
finally:
session.close()
except Exception as e:
logger.error(f"Failed to count remaining matches in fixture {fixture_id}: {e}")
return 0
def _ensure_minimum_matches_in_fixture(self, fixture_id: str, minimum_required: int):
"""Ensure fixture has at least minimum_required matches by creating new ones from old completed matches"""
try:
logger.info(f"🔄 Ensuring fixture {fixture_id} has at least {minimum_required} matches")
session = self.db_manager.get_session()
try:
# Get the last played match ID to exclude it from selection
last_played_match_id = self._get_last_played_match_id(fixture_id, session)
logger.info(f"🎯 Last played match ID: {last_played_match_id}")
# Select random completed matches, excluding the last played one
old_matches = self._select_random_completed_matches_excluding_last(
minimum_required, last_played_match_id, session
)
if old_matches:
logger.info(f"📋 Selected {len(old_matches)} old matches to create new ones")
self._create_matches_from_old_matches(fixture_id, old_matches, session)
logger.info(f"✅ Created {len(old_matches)} new matches in fixture {fixture_id}")
else:
logger.warning(f"⚠️ No suitable old matches found to create new ones for fixture {fixture_id}")
finally:
session.close()
except Exception as e:
logger.error(f"❌ Failed to ensure minimum matches in fixture {fixture_id}: {e}")
def _get_last_played_match_id(self, fixture_id: str, session) -> Optional[int]:
"""Get the ID of the last match that was played in this fixture"""
try:
# Find the most recently completed match in this fixture
last_match = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.status.in_(['done', 'cancelled', 'failed']),
MatchModel.active_status == True
).order_by(MatchModel.updated_at.desc()).first()
if last_match:
logger.debug(f"Last played match in fixture {fixture_id}: #{last_match.match_number} (ID: {last_match.id})")
return last_match.id
else:
logger.debug(f"No completed matches found in fixture {fixture_id}")
return None
except Exception as e:
logger.error(f"Failed to get last played match ID for fixture {fixture_id}: {e}")
return None
def _select_random_completed_matches_excluding_last(self, count: int, exclude_match_id: Optional[int], session) -> List[MatchModel]:
"""Select random completed matches from the database, excluding a specific match ID"""
try:
# Build query for completed matches
query = session.query(MatchModel).filter(
MatchModel.status.in_(['done', 'cancelled', 'failed']),
MatchModel.active_status == True
)
# Exclude the specified match if provided
if exclude_match_id:
query = query.filter(MatchModel.id != exclude_match_id)
completed_matches = query.all()
if len(completed_matches) < count:
logger.warning(f"Only {len(completed_matches)} completed matches available (excluding last played), requested {count}")
return completed_matches
# Select random matches
import random
selected_matches = random.sample(completed_matches, count)
logger.info(f"Selected {len(selected_matches)} random completed matches (excluding match ID {exclude_match_id})")
return selected_matches
except Exception as e:
logger.error(f"Failed to select random completed matches excluding {exclude_match_id}: {e}")
return []
def _cleanup_previous_match_extractions(self):
"""Clean up all previous unzipped match directories from temporary location"""
try:
import tempfile
from pathlib import Path
import shutil
temp_base = Path(tempfile.gettempdir())
logger.info(f"DEBUG: Cleaning up previous match extractions in: {temp_base}")
# Find all directories matching the pattern match_*_*
match_dirs = list(temp_base.glob("match_*_*"))
logger.info(f"DEBUG: Found {len(match_dirs)} match extraction directories to clean up")
cleaned_count = 0
for match_dir in match_dirs:
try:
if match_dir.is_dir():
logger.debug(f"DEBUG: Removing match extraction directory: {match_dir}")
shutil.rmtree(match_dir)
cleaned_count += 1
except Exception as dir_error:
logger.warning(f"DEBUG: Failed to remove directory {match_dir}: {dir_error}")
if cleaned_count > 0:
logger.info(f"DEBUG: Successfully cleaned up {cleaned_count} previous match extraction directories")
else:
logger.debug("DEBUG: No previous match extraction directories found to clean up")
except Exception as e:
logger.error(f"DEBUG: Failed to cleanup previous match extractions: {e}")
def _cleanup(self): def _cleanup(self):
"""Perform cleanup operations""" """Perform cleanup operations"""
try: try:
......
...@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent):
self.timer_duration_seconds = 0 self.timer_duration_seconds = 0
self.current_fixture_id: Optional[str] = None self.current_fixture_id: Optional[str] = None
self.current_match_id: Optional[int] = None self.current_match_id: Optional[int] = None
self.pending_match_id: Optional[int] = None # Match prepared by START_INTRO
# Synchronization # Synchronization
self._timer_lock = threading.RLock() self._timer_lock = threading.RLock()
...@@ -44,6 +45,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -44,6 +45,7 @@ class MatchTimerComponent(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games) self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message) self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
self.message_bus.subscribe(self.name, MessageType.NEXT_MATCH, self._handle_next_match) self.message_bus.subscribe(self.name, MessageType.NEXT_MATCH, self._handle_next_match)
self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro)
logger.info("MatchTimer component initialized") logger.info("MatchTimer component initialized")
...@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent):
self._handle_custom_message(message) self._handle_custom_message(message)
elif message.type == MessageType.NEXT_MATCH: elif message.type == MessageType.NEXT_MATCH:
self._handle_next_match(message) self._handle_next_match(message)
elif message.type == MessageType.START_INTRO:
self._handle_start_intro(message)
except Exception as e: except Exception as e:
logger.error(f"Failed to process message: {e}") logger.error(f"Failed to process message: {e}")
...@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent):
self.timer_start_time = None self.timer_start_time = None
self.current_fixture_id = None self.current_fixture_id = None
self.current_match_id = None self.current_match_id = None
self.pending_match_id = None
# Unregister from message bus # Unregister from message bus
self.message_bus.unregister_component(self.name) self.message_bus.unregister_component(self.name)
...@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent):
logger.error(f"Failed to handle custom message: {e}") logger.error(f"Failed to handle custom message: {e}")
def _handle_next_match(self, message: Message): def _handle_next_match(self, message: Message):
"""Handle NEXT_MATCH message - start the next match in sequence""" """Handle NEXT_MATCH message - restart timer for next match interval"""
try: try:
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id") match_id = message.data.get("match_id")
logger.info(f"Received NEXT_MATCH message for fixture {fixture_id}, match {match_id}") logger.info(f"Received NEXT_MATCH message for fixture {fixture_id}, match {match_id}")
logger.info("Previous match completed - restarting timer for next interval")
# Find and start the next match # Find and start the next match
match_info = self._find_and_start_next_match() match_info = self._find_and_start_next_match()
...@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent):
# Reset timer for next interval # Reset timer for next interval
match_interval = self._get_match_interval() match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, match_info['fixture_id']) self._start_timer(match_interval * 60, match_info['fixture_id'])
logger.info(f"Timer restarted for {match_interval} minute interval")
else: else:
logger.info("No more matches to start, stopping timer") logger.info("No more matches to start, stopping timer")
self._stop_timer() self._stop_timer()
except Exception as e: except Exception as e:
logger.error(f"Failed to handle NEXT_MATCH message: {e}") logger.error(f"Failed to handle NEXT_MATCH message: {e}")
# On error, try to restart timer anyway
try:
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, self.current_fixture_id)
except Exception as restart_e:
logger.error(f"Failed to restart timer after NEXT_MATCH error: {restart_e}")
def _handle_start_intro(self, message: Message):
"""Handle START_INTRO message - store the match_id for later MATCH_START"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Received START_INTRO message for fixture {fixture_id}, match {match_id}")
# Store the match_id for when timer expires
with self._timer_lock:
self.pending_match_id = match_id
self.current_fixture_id = fixture_id
logger.info(f"Stored pending match_id {match_id} for timer expiration")
except Exception as e:
logger.error(f"Failed to handle START_INTRO message: {e}")
def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]): def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]):
"""Start the countdown timer""" """Start the countdown timer"""
...@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent):
self._send_timer_update() self._send_timer_update()
def _on_timer_expired(self): def _on_timer_expired(self):
"""Handle timer expiration - start next match""" """Handle timer expiration - start the match that was prepared by START_INTRO"""
try: try:
logger.info("Match timer expired, starting next match...") logger.info("Match timer expired, starting prepared match...")
# Find and start the next match with self._timer_lock:
match_info = self._find_and_start_next_match() pending_match_id = self.pending_match_id
fixture_id = self.current_fixture_id
if match_info: if pending_match_id:
logger.info(f"Started match {match_info['match_id']} in fixture {match_info['fixture_id']}") # Send MATCH_START message for the prepared match
match_start_message = MessageBuilder.match_start(
sender=self.name,
fixture_id=fixture_id,
match_id=pending_match_id
)
# Reset timer for next interval self.message_bus.publish(match_start_message)
match_interval = self._get_match_interval() logger.info(f"Sent MATCH_START for prepared match {pending_match_id} in fixture {fixture_id}")
self._start_timer(match_interval * 60, match_info['fixture_id']) logger.info("Timer stopped - will restart after match completion (NEXT_MATCH)")
# Clear the pending match and stop the timer
with self._timer_lock:
self.pending_match_id = None
self._stop_timer()
else: else:
logger.info("No more matches to start, stopping timer") logger.warning("No pending match prepared by START_INTRO, stopping timer")
self._stop_timer() self._stop_timer()
except Exception as e: except Exception as e:
logger.error(f"Failed to handle timer expiration: {e}") logger.error(f"Failed to handle timer expiration: {e}")
# Reset timer on error # Stop timer on error - don't restart automatically
match_interval = self._get_match_interval() self._stop_timer()
self._start_timer(match_interval * 60, self.current_fixture_id)
def _find_and_start_next_match(self) -> Optional[Dict[str, Any]]: def _find_and_start_next_match(self) -> Optional[Dict[str, Any]]:
"""Find and start the next available match""" """Find and start the next available match"""
...@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent):
target_fixture_id = target_match.fixture_id target_fixture_id = target_match.fixture_id
if target_match: if target_match:
# Send MATCH_START message # Send START_INTRO message
match_start_message = MessageBuilder.match_start( start_intro_message = MessageBuilder.start_intro(
sender=self.name, sender=self.name,
fixture_id=target_fixture_id or target_match.fixture_id, fixture_id=target_fixture_id or target_match.fixture_id,
match_id=target_match.id match_id=target_match.id
) )
self.message_bus.publish(match_start_message) self.message_bus.publish(start_intro_message)
return { return {
"fixture_id": target_fixture_id or target_match.fixture_id, "fixture_id": target_fixture_id or target_match.fixture_id,
......
...@@ -70,6 +70,7 @@ class MessageType(Enum): ...@@ -70,6 +70,7 @@ class MessageType(Enum):
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH" PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE" PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
PLAY_VIDEO_RESULT = "PLAY_VIDEO_RESULT" PLAY_VIDEO_RESULT = "PLAY_VIDEO_RESULT"
PLAY_VIDEO_RESULT_DONE = "PLAY_VIDEO_RESULT_DONE"
MATCH_DONE = "MATCH_DONE" MATCH_DONE = "MATCH_DONE"
NEXT_MATCH = "NEXT_MATCH" NEXT_MATCH = "NEXT_MATCH"
GAME_STATUS = "GAME_STATUS" GAME_STATUS = "GAME_STATUS"
...@@ -661,11 +662,27 @@ class MessageBuilder: ...@@ -661,11 +662,27 @@ class MessageBuilder:
) )
@staticmethod @staticmethod
def play_video_result(sender: str, fixture_id: str, match_id: int, result: str) -> Message: def play_video_result(sender: str, fixture_id: str, match_id: int, result: str, under_over_result: Optional[str] = None) -> Message:
"""Create PLAY_VIDEO_RESULT message""" """Create PLAY_VIDEO_RESULT message"""
data = {
"fixture_id": fixture_id,
"match_id": match_id,
"result": result
}
if under_over_result is not None:
data["under_over_result"] = under_over_result
return Message( return Message(
type=MessageType.PLAY_VIDEO_RESULT, type=MessageType.PLAY_VIDEO_RESULT,
sender=sender, sender=sender,
data=data
)
@staticmethod
def play_video_result_done(sender: str, fixture_id: str, match_id: int, result: str) -> Message:
"""Create PLAY_VIDEO_RESULT_DONE message"""
return Message(
type=MessageType.PLAY_VIDEO_RESULT_DONE,
sender=sender,
data={ data={
"fixture_id": fixture_id, "fixture_id": fixture_id,
"match_id": match_id, "match_id": match_id,
...@@ -674,15 +691,18 @@ class MessageBuilder: ...@@ -674,15 +691,18 @@ class MessageBuilder:
) )
@staticmethod @staticmethod
def match_done(sender: str, fixture_id: str, match_id: int) -> Message: def match_done(sender: str, fixture_id: str, match_id: int, result: Optional[str] = None) -> Message:
"""Create MATCH_DONE message""" """Create MATCH_DONE message"""
data = {
"fixture_id": fixture_id,
"match_id": match_id
}
if result is not None:
data["result"] = result
return Message( return Message(
type=MessageType.MATCH_DONE, type=MessageType.MATCH_DONE,
sender=sender, sender=sender,
data={ data=data
"fixture_id": fixture_id,
"match_id": match_id
}
) )
@staticmethod @staticmethod
......
...@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent):
self.broadcast_port = broadcast_port self.broadcast_port = broadcast_port
self.broadcast_interval = 30.0 # 30 seconds self.broadcast_interval = 30.0 # 30 seconds
self.socket: Optional[socket.socket] = None self.socket: Optional[socket.socket] = None
# Server information to broadcast # Server information to broadcast
self.server_info: Dict[str, Any] = { self.server_info: Dict[str, Any] = {
"service": "MBetterClient", "service": "MBetterClient",
...@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent):
"url": "http://127.0.0.1:5001", "url": "http://127.0.0.1:5001",
"timestamp": time.time() "timestamp": time.time()
} }
# Shutdown event for responsive shutdown
self.shutdown_event = threading.Event()
# Register message queue # Register message queue
self.message_queue = self.message_bus.register_component(self.name) self.message_queue = self.message_bus.register_component(self.name)
logger.info(f"UDP Broadcast component initialized on port {broadcast_port}") logger.info(f"UDP Broadcast component initialized on port {broadcast_port}")
def initialize(self) -> bool: def initialize(self) -> bool:
...@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Set timeout to prevent blocking operations
self.socket.settimeout(1.0)
# Clear shutdown event
self.shutdown_event.clear()
# Subscribe to system status messages to get web server info # Subscribe to system status messages to get web server info
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status) self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
logger.info("UDP broadcast socket initialized successfully") logger.info("UDP broadcast socket initialized successfully")
return True return True
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast initialization failed: {e}") logger.error(f"UDP broadcast initialization failed: {e}")
return False return False
...@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent):
last_broadcast_time = 0 last_broadcast_time = 0
while self.running: while self.running and not self.shutdown_event.is_set():
try: try:
current_time = time.time() current_time = time.time()
# Process messages # Process messages with shorter timeout for responsive shutdown
message = self.message_bus.get_message(self.name, timeout=1.0) message = self.message_bus.get_message(self.name, timeout=0.5)
if message: if message:
self._process_message(message) self._process_message(message)
# Broadcast every 30 seconds # Broadcast every 30 seconds
if current_time - last_broadcast_time >= self.broadcast_interval: if current_time - last_broadcast_time >= self.broadcast_interval:
self._broadcast_server_info() self._broadcast_server_info()
last_broadcast_time = current_time last_broadcast_time = current_time
# Update heartbeat # Update heartbeat
self.heartbeat() self.heartbeat()
time.sleep(0.5) # Check shutdown event more frequently
if self.shutdown_event.wait(0.5):
break
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast loop error: {e}") logger.error(f"UDP broadcast loop error: {e}")
time.sleep(2.0) # Shorter sleep on error to be more responsive to shutdown
if not self.shutdown_event.wait(1.0):
continue
break
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast run failed: {e}") logger.error(f"UDP broadcast run failed: {e}")
...@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent):
"""Shutdown UDP broadcast component""" """Shutdown UDP broadcast component"""
try: try:
logger.info("Shutting down UDP broadcast...") logger.info("Shutting down UDP broadcast...")
# Signal shutdown event to wake up the main loop
self.shutdown_event.set()
# Close socket to prevent further operations
if self.socket: if self.socket:
self.socket.close() try:
self.socket = None self.socket.close()
except Exception as e:
logger.debug(f"Error closing UDP socket: {e}")
finally:
self.socket = None
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast shutdown error: {e}") logger.error(f"UDP broadcast shutdown error: {e}")
...@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent):
for broadcast_addr in broadcast_addresses: for broadcast_addr in broadcast_addresses:
try: try:
self.socket.sendto(broadcast_data, (broadcast_addr, self.broadcast_port)) if self.socket and not self.shutdown_event.is_set():
logger.debug(f"Broadcasted to {broadcast_addr}:{self.broadcast_port}") self.socket.sendto(broadcast_data, (broadcast_addr, self.broadcast_port))
logger.debug(f"Broadcasted to {broadcast_addr}:{self.broadcast_port}")
else:
break # Exit if shutting down or socket closed
except socket.timeout:
logger.debug(f"Timeout broadcasting to {broadcast_addr}")
except OSError as e:
if self.shutdown_event.is_set():
break # Exit if shutting down
logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}")
except Exception as e: except Exception as e:
logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}") logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}")
......
...@@ -237,6 +237,38 @@ class OverlayWebChannel(QObject): ...@@ -237,6 +237,38 @@ class OverlayWebChannel(QObject):
logger.error(f"Failed to get timer state: {e}") logger.error(f"Failed to get timer state: {e}")
return json.dumps({"running": False, "remaining_seconds": 0}) return json.dumps({"running": False, "remaining_seconds": 0})
@pyqtSlot(int, result=str)
def getWinningBets(self, match_id: int) -> str:
"""Provide winning bets data for a match to JavaScript via WebChannel"""
try:
logger.info(f"QtWebChannel: Getting winning bets for match {match_id}")
# Get winning bets from database
winning_bets = self._get_winning_bets_from_database(match_id)
logger.debug(f"QtWebChannel: Found {len(winning_bets)} winning bets for match {match_id}")
return json.dumps(winning_bets)
except Exception as e:
logger.error(f"QtWebChannel: Failed to get winning bets for match {match_id}: {e}")
return json.dumps([])
@pyqtSlot(int, result=str)
def getWinningOutcomes(self, match_id: int) -> str:
"""Provide winning outcomes data for a match to JavaScript via WebChannel"""
try:
logger.info(f"QtWebChannel: Getting winning outcomes for match {match_id}")
# Get winning outcomes from database
winning_outcomes = self._get_winning_outcomes_from_database(match_id)
logger.debug(f"QtWebChannel: Found {len(winning_outcomes)} winning outcomes for match {match_id}")
return json.dumps(winning_outcomes)
except Exception as e:
logger.error(f"QtWebChannel: Failed to get winning outcomes for match {match_id}: {e}")
return json.dumps([])
def _get_fixture_data_from_games_thread(self) -> Optional[List[Dict[str, Any]]]: def _get_fixture_data_from_games_thread(self) -> Optional[List[Dict[str, Any]]]:
"""Get fixture data from the games thread""" """Get fixture data from the games thread"""
try: try:
...@@ -333,6 +365,93 @@ class OverlayWebChannel(QObject): ...@@ -333,6 +365,93 @@ class OverlayWebChannel(QObject):
logger.error(f"Failed to get fixture data from database: {e}") logger.error(f"Failed to get fixture data from database: {e}")
return None return None
def _get_winning_bets_from_database(self, match_id: int) -> List[Dict[str, Any]]:
"""Get winning bets for a match from database"""
try:
from ..database.models import BetDetailModel, MatchModel
from datetime import datetime
# Use the database manager passed to this channel
if not self.db_manager:
logger.error("Database manager not initialized")
return []
session = self.db_manager.get_session()
try:
# Get winning bets for this match
winning_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'win',
BetDetailModel.active_status == True
).all()
# Convert to dictionary format for JavaScript
bets_data = []
for bet in winning_bets:
bet_data = {
'id': bet.id,
'outcome': bet.outcome,
'amount': float(bet.amount),
'win_amount': float(bet.win_amount) if bet.win_amount else 0.0,
'result': bet.result
}
bets_data.append(bet_data)
logger.debug(f"Retrieved {len(bets_data)} winning bets for match {match_id}")
return bets_data
finally:
session.close()
except Exception as e:
logger.error(f"Failed to get winning bets from database: {e}")
return []
def _get_winning_outcomes_from_database(self, match_id: int) -> List[Dict[str, Any]]:
"""Get winning outcomes aggregated by outcome type for a match from database"""
try:
from ..database.models import BetDetailModel, MatchModel
from sqlalchemy import func
# Use the database manager passed to this channel
if not self.db_manager:
logger.error("Database manager not initialized")
return []
session = self.db_manager.get_session()
try:
# Get aggregated winning amounts by outcome for this match
winning_outcomes_query = session.query(
BetDetailModel.outcome,
func.sum(BetDetailModel.win_amount).label('total_amount')
).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'win',
BetDetailModel.active_status == True,
MatchModel.active_status == True
).group_by(BetDetailModel.outcome).all()
# Convert to dictionary format for JavaScript
outcomes_data = []
for outcome_name, total_amount in winning_outcomes_query:
outcome_data = {
'outcome': outcome_name,
'amount': float(total_amount) if total_amount else 0.0
}
outcomes_data.append(outcome_data)
logger.debug(f"Retrieved {len(outcomes_data)} winning outcomes for match {match_id}")
return outcomes_data
finally:
session.close()
except Exception as e:
logger.error(f"Failed to get winning outcomes from database: {e}")
return []
...@@ -497,12 +616,16 @@ class OverlayWebView(QWebEngineView): ...@@ -497,12 +616,16 @@ class OverlayWebView(QWebEngineView):
# Enable Qt WebEngine developer tools # Enable Qt WebEngine developer tools
page = self.page() page = self.page()
if page: if page:
# Try to connect to Qt WebEngine console messages # Try to connect to Qt WebEngine console messages (may not be available in all Qt versions)
try: try:
page.javaScriptConsoleMessage.connect(self._on_javaScript_console_message) # Check if javaScriptConsoleMessage is available and is a signal
logger.info("Connected to Qt WebEngine console messages") if hasattr(page, 'javaScriptConsoleMessage') and hasattr(page.javaScriptConsoleMessage, 'connect'):
page.javaScriptConsoleMessage.connect(self._on_javaScript_console_message)
logger.info("Connected to Qt WebEngine console messages")
else:
logger.debug("Qt WebEngine console messages not available or not a signal in this Qt version")
except Exception as e: except Exception as e:
logger.warning(f"Could not connect to Qt WebEngine console messages: {e}") logger.debug(f"Could not connect to Qt WebEngine console messages (this is normal in some Qt configurations): {e}")
# Override console.log in JavaScript to send messages to Python # Override console.log in JavaScript to send messages to Python
page.runJavaScript(""" page.runJavaScript("""
...@@ -1327,7 +1450,7 @@ class PlayerWindow(QMainWindow): ...@@ -1327,7 +1450,7 @@ class PlayerWindow(QMainWindow):
position_changed = pyqtSignal(int, int) position_changed = pyqtSignal(int, int)
video_loaded = pyqtSignal(str) video_loaded = pyqtSignal(str)
def __init__(self, settings: QtConfig, message_bus: MessageBus = None, debug_overlay: bool = False): def __init__(self, settings: QtConfig, message_bus: MessageBus = None, debug_overlay: bool = False, qt_player=None):
super().__init__() super().__init__()
self.settings = settings self.settings = settings
self.debug_overlay = debug_overlay self.debug_overlay = debug_overlay
...@@ -1335,6 +1458,7 @@ class PlayerWindow(QMainWindow): ...@@ -1335,6 +1458,7 @@ class PlayerWindow(QMainWindow):
self.thread_pool = QThreadPool() self.thread_pool = QThreadPool()
self.thread_pool.setMaxThreadCount(4) self.thread_pool.setMaxThreadCount(4)
self._message_bus = message_bus # Store message bus reference for shutdown messages self._message_bus = message_bus # Store message bus reference for shutdown messages
self.qt_player = qt_player # Reference to parent QtVideoPlayer for message sending
self.setup_ui() self.setup_ui()
self.setup_media_player() self.setup_media_player()
...@@ -1849,8 +1973,8 @@ class PlayerWindow(QMainWindow): ...@@ -1849,8 +1973,8 @@ class PlayerWindow(QMainWindow):
logger.error(f"Failed to load template after video start: {template_error}") logger.error(f"Failed to load template after video start: {template_error}")
# Continue without template - video should still play # Continue without template - video should still play
# Schedule template loading to happen after video has started (2 seconds delay) # Schedule template loading to happen after video has started (0.3 seconds delay)
QTimer.singleShot(2000, load_template_after_video_start) QTimer.singleShot(300, load_template_after_video_start)
# CRITICAL FIX: Force video widget refresh after template operations # CRITICAL FIX: Force video widget refresh after template operations
if video_widget: if video_widget:
...@@ -1870,7 +1994,7 @@ class PlayerWindow(QMainWindow): ...@@ -1870,7 +1994,7 @@ class PlayerWindow(QMainWindow):
if hasattr(self, 'window_overlay'): if hasattr(self, 'window_overlay'):
overlay_view = self.window_overlay overlay_view = self.window_overlay
# Update overlay safely - handles both native and WebEngine # Update overlay safely - handles both native and WebEngine
QTimer.singleShot(1000, lambda: self._update_overlay_safe(overlay_view, overlay_data)) QTimer.singleShot(300, lambda: self._update_overlay_safe(overlay_view, overlay_data))
# Start template rotation timer if enabled # Start template rotation timer if enabled
if self.template_sequence and len(self.template_sequence) > 1: if self.template_sequence and len(self.template_sequence) > 1:
...@@ -2046,7 +2170,12 @@ class PlayerWindow(QMainWindow): ...@@ -2046,7 +2170,12 @@ class PlayerWindow(QMainWindow):
# Check if this is the end of a match video # Check if this is the end of a match video
if self.is_playing_match_video: if self.is_playing_match_video:
logger.info(f"MATCH DEBUG: Match video ended - sending PLAY_VIDEO_MATCH_DONE") logger.info(f"MATCH DEBUG: Match video ended - sending PLAY_VIDEO_MATCH_DONE")
self._send_match_video_done_message() if self.qt_player:
self.qt_player._send_match_video_done_message(
match_id=self.current_match_id,
video_filename=self.current_match_video_filename,
fixture_id=self.current_fixture_id
)
# Reset match video tracking # Reset match video tracking
self.is_playing_match_video = False self.is_playing_match_video = False
self.current_match_id = None self.current_match_id = None
...@@ -2054,6 +2183,20 @@ class PlayerWindow(QMainWindow): ...@@ -2054,6 +2183,20 @@ class PlayerWindow(QMainWindow):
self.current_fixture_id = None self.current_fixture_id = None
return return
# Check if this is the end of a result video (any result video)
current_filename = Path(self.current_file_path).name if self.current_file_path else ""
result_videos = ["OVER.mp4", "UNDER.mp4", "WIN1.mp4", "WIN2.mp4", "KO1.mp4", "KO2.mp4", "DRAW.mp4", "RET1.mp4", "RET2.mp4"]
if current_filename in result_videos and hasattr(self, 'current_result_video_info') and self.current_result_video_info:
logger.info(f"RESULT DEBUG: Result video {current_filename} ended - waiting 5 seconds then sending PLAY_VIDEO_RESULTS_DONE")
# Wait 5 seconds to allow results overlay template to show results
import time
time.sleep(5)
if self.qt_player:
self.qt_player._send_result_video_done_message(result_info=self.current_result_video_info)
# Reset result video tracking
self.current_result_video_info = None
return
# Handle loop functionality for intro videos # Handle loop functionality for intro videos
if self.loop_enabled: if self.loop_enabled:
logger.debug(f"LOOP DEBUG: Processing loop restart logic...") logger.debug(f"LOOP DEBUG: Processing loop restart logic...")
...@@ -2455,7 +2598,7 @@ class QtVideoPlayer(QObject): ...@@ -2455,7 +2598,7 @@ class QtVideoPlayer(QObject):
self._configure_linux_app_settings() self._configure_linux_app_settings()
# Create player window with message bus reference and debug settings # Create player window with message bus reference and debug settings
self.window = PlayerWindow(self.settings, self.message_bus, debug_overlay=self.debug_overlay) self.window = PlayerWindow(self.settings, self.message_bus, debug_overlay=self.debug_overlay, qt_player=self)
# CRITICAL: Connect signal to slot for cross-thread video playback # CRITICAL: Connect signal to slot for cross-thread video playback
self.play_video_signal.connect(self.window.play_video, Qt.ConnectionType.QueuedConnection) self.play_video_signal.connect(self.window.play_video, Qt.ConnectionType.QueuedConnection)
...@@ -2516,7 +2659,7 @@ class QtVideoPlayer(QObject): ...@@ -2516,7 +2659,7 @@ class QtVideoPlayer(QObject):
logger.info("QtPlayer subscriptions completed successfully") logger.info("QtPlayer subscriptions completed successfully")
# Delay loading default overlay to allow JavaScript initialization # Delay loading default overlay to allow JavaScript initialization
QTimer.singleShot(2000, self._load_default_overlay) # Wait 2 seconds QTimer.singleShot(300, self._load_default_overlay) # Wait 0.3 seconds
# Request initial game status to check if we should play intro # Request initial game status to check if we should play intro
QTimer.singleShot(3000, self._request_initial_game_status) # Wait 3 seconds for everything to be ready QTimer.singleShot(3000, self._request_initial_game_status) # Wait 3 seconds for everything to be ready
...@@ -3343,7 +3486,7 @@ class QtVideoPlayer(QObject): ...@@ -3343,7 +3486,7 @@ class QtVideoPlayer(QObject):
logger.error(f"Failed to handle START_INTRO message: {e}") logger.error(f"Failed to handle START_INTRO message: {e}")
def _handle_play_video_match(self, message: Message): def _handle_play_video_match(self, message: Message):
"""Handle PLAY_VIDEO_MATCH message - stop intro loop and play match video""" """Handle PLAY_VIDEO_MATCH message - play match video with overlay template and wait for messages"""
try: try:
logger.info("Handling PLAY_VIDEO_MATCH message") logger.info("Handling PLAY_VIDEO_MATCH message")
...@@ -3376,17 +3519,28 @@ class QtVideoPlayer(QObject): ...@@ -3376,17 +3519,28 @@ class QtVideoPlayer(QObject):
self.window.template_rotation_timer.stop() self.window.template_rotation_timer.stop()
logger.info("Template rotation timer stopped for match video") logger.info("Template rotation timer stopped for match video")
# Ensure the match ZIP file is extracted before trying to find the video
logger.info(f"Ensuring ZIP file is extracted for match {match_id} before finding video")
self._unzip_match_zip_file(match_id)
# Find the match video file from the ZIP # Find the match video file from the ZIP
logger.info(f"Looking for match video: {video_filename} for match {match_id}")
match_video_path = self._find_match_video_file(match_id, video_filename) match_video_path = self._find_match_video_file(match_id, video_filename)
if match_video_path:
logger.info(f"Successfully found match video: {match_video_path}")
else:
logger.error(f"Match video not found: {video_filename} for match {match_id}")
logger.error("ZIP extraction may have failed or video file may not exist in ZIP")
if match_video_path: if match_video_path:
logger.info(f"Found match video: {match_video_path}") logger.info(f"Found match video: {match_video_path}")
# Set match video tracking flags # Set match video tracking flags on PlayerWindow
self.is_playing_match_video = True self.window.is_playing_match_video = True
self.current_match_id = match_id self.window.current_match_id = match_id
self.current_match_video_filename = video_filename self.window.current_match_video_filename = video_filename
self.current_fixture_id = fixture_id self.window.current_fixture_id = fixture_id
# Reset loop state for match videos (they don't loop) # Reset loop state for match videos (they don't loop)
self.loop_enabled = False self.loop_enabled = False
...@@ -3396,20 +3550,25 @@ class QtVideoPlayer(QObject): ...@@ -3396,20 +3550,25 @@ class QtVideoPlayer(QObject):
# Play the match video (no looping) # Play the match video (no looping)
if self.window: if self.window:
# Update overlay with result information # Update overlay with result information and apply overlay template
overlay_data = { overlay_data = {
"title": f"Match {match_id}", "title": f"Match {match_id}",
"subtitle": f"Result: {result}" if result else "Live Action", "subtitle": f"Result: {result}" if result else "Live Action",
"result": result "result": result,
"match_id": match_id,
"fixture_id": fixture_id
} }
self.window.play_video( self.window.play_video(
str(match_video_path), str(match_video_path),
template_data=overlay_data, template_data=overlay_data,
template_name="match_video.html", template_name="match_video.html", # Use match_video template for overlay
loop_data=None # No looping for match videos loop_data=None # No looping for match videos
) )
logger.info(f"Match video started: {video_filename} with result: {result}") logger.info(f"Match video started: {video_filename} with result: {result} and overlay template applied")
# Player should now wait for other messages while video plays
# Video play will continue until PLAY_VIDEO_MATCH_DONE is sent
else: else:
logger.error("No window available for match video playback") logger.error("No window available for match video playback")
else: else:
...@@ -3567,6 +3726,118 @@ class QtVideoPlayer(QObject): ...@@ -3567,6 +3726,118 @@ class QtVideoPlayer(QObject):
logger.error(f"Failed to find intro video file: {e}") logger.error(f"Failed to find intro video file: {e}")
return None return None
def _get_database_manager(self):
"""Get database manager from message bus"""
try:
if hasattr(self, '_message_bus') and self._message_bus:
# Try to get db_manager from web_dashboard component
try:
web_dashboard_queue = self._message_bus._queues.get('web_dashboard')
if web_dashboard_queue and hasattr(web_dashboard_queue, 'component'):
component = web_dashboard_queue.component
if hasattr(component, 'db_manager'):
logger.debug("QtVideoPlayer: Got db_manager from web_dashboard component")
return component.db_manager
except Exception as e:
logger.debug(f"QtVideoPlayer: Could not get db_manager from message bus: {e}")
# Fallback: create database manager directly
from ..config.settings import get_user_data_dir
from ..database.manager import DatabaseManager
db_path = get_user_data_dir() / "mbetterclient.db"
logger.debug(f"QtVideoPlayer: Creating database manager directly: {db_path}")
db_manager = DatabaseManager(str(db_path))
if db_manager.initialize():
return db_manager
else:
logger.warning("QtVideoPlayer: Failed to initialize database manager")
return None
except Exception as e:
logger.error(f"QtVideoPlayer: Failed to get database manager: {e}")
return None
def _unzip_match_zip_file(self, match_id: int):
"""Unzip the ZIP file associated with a match to a temporary directory"""
try:
import zipfile
import tempfile
import os
from pathlib import Path
logger.info(f"DEBUG: Starting ZIP extraction for match {match_id}")
# Get database manager from message bus
db_manager = self._get_database_manager()
if not db_manager:
logger.error("DEBUG: No database manager available for ZIP extraction")
return
session = db_manager.get_session()
try:
# Get the match from database
from ..database.models import MatchModel
match = session.query(MatchModel).filter_by(id=match_id).first()
if not match:
logger.warning(f"DEBUG: Match {match_id} not found in database, skipping ZIP extraction")
return
logger.info(f"DEBUG: Found match {match_id}, zip_filename: {match.zip_filename}")
if not match.zip_filename:
logger.info(f"DEBUG: Match {match_id} has no associated ZIP file, skipping extraction")
return
# Determine ZIP file location (ZIP files are stored in the zip_files directory)
from ..config.settings import get_user_data_dir
user_data_dir = get_user_data_dir()
zip_file_path = user_data_dir / "zip_files" / match.zip_filename
logger.info(f"DEBUG: Looking for ZIP file at: {zip_file_path}")
logger.info(f"DEBUG: ZIP file exists: {zip_file_path.exists()}")
if not zip_file_path.exists():
logger.warning(f"DEBUG: ZIP file not found: {zip_file_path}")
return
logger.info(f"DEBUG: ZIP file size: {zip_file_path.stat().st_size} bytes")
# Create temporary directory for extraction
temp_dir = Path(tempfile.mkdtemp(prefix=f"match_{match_id}_"))
logger.info(f"DEBUG: Created temp directory: {temp_dir}")
# Extract the ZIP file
logger.info(f"DEBUG: Starting ZIP extraction...")
with zipfile.ZipFile(str(zip_file_path), 'r') as zip_ref:
file_list = zip_ref.namelist()
logger.info(f"DEBUG: ZIP contains {len(file_list)} files: {file_list}")
zip_ref.extractall(str(temp_dir))
# Log extraction results
extracted_files = list(temp_dir.rglob("*"))
logger.info(f"DEBUG: Successfully extracted {len(extracted_files)} files from {match.zip_filename}")
for extracted_file in extracted_files:
if extracted_file.is_file():
logger.info(f"DEBUG: Extracted file: {extracted_file} (size: {extracted_file.stat().st_size} bytes)")
# Store the temporary directory path for potential cleanup
# In a real implementation, you might want to track this for cleanup
match.temp_extract_path = str(temp_dir)
# Update match in database with temp path (optional)
session.commit()
logger.info(f"DEBUG: ZIP extraction completed for match {match_id}")
finally:
session.close()
except Exception as e:
logger.error(f"DEBUG: Failed to unzip ZIP file for match {match_id}: {e}")
import traceback
logger.error(f"DEBUG: Full traceback: {traceback.format_exc()}")
def _find_match_video_file(self, match_id: int, video_filename: str) -> Optional[Path]: def _find_match_video_file(self, match_id: int, video_filename: str) -> Optional[Path]:
"""Find the match video file from the unzipped ZIP""" """Find the match video file from the unzipped ZIP"""
try: try:
...@@ -3576,33 +3847,47 @@ class QtVideoPlayer(QObject): ...@@ -3576,33 +3847,47 @@ class QtVideoPlayer(QObject):
# Look for temp directories created by _unzip_match_zip_file # Look for temp directories created by _unzip_match_zip_file
temp_base = Path(tempfile.gettempdir()) temp_base = Path(tempfile.gettempdir())
temp_dir_pattern = f"match_{match_id}_" temp_dir_pattern = f"match_{match_id}_"
logger.debug(f"DEBUG: Looking for temp directories with pattern '{temp_dir_pattern}' in {temp_base}") logger.info(f"DEBUG: Looking for match video '{video_filename}' for match {match_id}")
logger.info(f"DEBUG: Searching in temp directory: {temp_base}")
logger.info(f"DEBUG: Looking for directories with pattern '{temp_dir_pattern}'")
matching_dirs = list(temp_base.glob(f"{temp_dir_pattern}*")) matching_dirs = list(temp_base.glob(f"{temp_dir_pattern}*"))
logger.debug(f"DEBUG: Found {len(matching_dirs)} directories matching pattern") logger.info(f"DEBUG: Found {len(matching_dirs)} directories matching pattern '{temp_dir_pattern}*'")
for temp_dir in matching_dirs: for temp_dir in matching_dirs:
if temp_dir.is_dir(): if temp_dir.is_dir():
logger.debug(f"DEBUG: Checking directory: {temp_dir}") logger.info(f"DEBUG: Checking directory: {temp_dir}")
contents = list(temp_dir.iterdir())
logger.info(f"DEBUG: Directory contents: {[str(f) for f in contents]}")
video_file = temp_dir / video_filename video_file = temp_dir / video_filename
logger.debug(f"DEBUG: Looking for video file: {video_file}") logger.info(f"DEBUG: Looking for video file: {video_file}")
if video_file.exists(): if video_file.exists():
logger.info(f"Found match video: {video_file} (size: {video_file.stat().st_size} bytes)") logger.info(f"DEBUG: FOUND match video: {video_file} (size: {video_file.stat().st_size} bytes)")
return video_file return video_file
else: else:
logger.debug(f"DEBUG: Video file not found in this directory") logger.warning(f"DEBUG: Video file {video_filename} not found in {temp_dir}")
logger.warning(f"Match video not found: {video_filename} for match {match_id}") logger.error(f"Match video not found: {video_filename} for match {match_id}")
logger.warning(f"DEBUG: Searched in {len(matching_dirs)} directories:") logger.error(f"DEBUG: Searched in {len(matching_dirs)} directories:")
for temp_dir in matching_dirs: for temp_dir in matching_dirs:
logger.warning(f" - {temp_dir}") logger.error(f" - {temp_dir} (exists: {temp_dir.exists()})")
if temp_dir.exists(): if temp_dir.exists():
contents = list(temp_dir.iterdir()) if temp_dir.is_dir() else [] contents = list(temp_dir.iterdir()) if temp_dir.is_dir() else []
logger.warning(f" Contents: {[str(f) for f in contents]}") logger.error(f" Contents: {[str(f) for f in contents]}")
# Also check if there are any match_* directories at all
all_match_dirs = list(temp_base.glob("match_*"))
logger.info(f"DEBUG: Total match directories in temp: {len(all_match_dirs)}")
for d in all_match_dirs[:5]: # Show first 5
logger.info(f" - {d}")
return None return None
except Exception as e: except Exception as e:
logger.error(f"Failed to find match video file: {e}") logger.error(f"Failed to find match video file: {e}")
import traceback
logger.error(f"DEBUG: Full traceback: {traceback.format_exc()}")
return None return None
def _start_template_rotation(self): def _start_template_rotation(self):
...@@ -3657,13 +3942,13 @@ class QtVideoPlayer(QObject): ...@@ -3657,13 +3942,13 @@ class QtVideoPlayer(QObject):
logger.error(f"Failed to rotate template: {e}") logger.error(f"Failed to rotate template: {e}")
def _handle_play_video_result(self, message: Message): def _handle_play_video_result(self, message: Message):
"""Handle PLAY_VIDEO_RESULT message - queue or play result video""" """Handle PLAY_VIDEO_RESULTS message - play result video with results overlay"""
try: try:
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id") match_id = message.data.get("match_id")
result = message.data.get("result") result = message.data.get("result")
logger.info(f"Handling PLAY_VIDEO_RESULT: fixture={fixture_id}, match={match_id}, result={result}") logger.info(f"Handling PLAY_VIDEO_RESULTS: fixture={fixture_id}, match={match_id}, result={result}")
# Find the result video file # Find the result video file
result_video_path = self._find_result_video_file(match_id, result) result_video_path = self._find_result_video_file(match_id, result)
...@@ -3687,20 +3972,24 @@ class QtVideoPlayer(QObject): ...@@ -3687,20 +3972,24 @@ class QtVideoPlayer(QObject):
} }
# Check if currently playing match video # Check if currently playing match video
currently_playing_match = (self.current_match_id is not None and currently_playing_match = (hasattr(self.window, 'current_match_id') and
self.current_match_video_filename is not None) self.window.current_match_id is not None and
hasattr(self.window, 'current_match_video_filename') and
self.window.current_match_video_filename is not None)
if currently_playing_match: if currently_playing_match:
# Queue the result video to play next # Stop the current match video and play result video immediately
logger.info("Match video currently playing, queuing result video") logger.info("Match video currently playing, stopping it and playing result video")
self._queue_result_video(result_video_info) if self.window and hasattr(self.window, 'media_player'):
self.window.stop_playback()
self._play_result_video(result_video_info)
else: else:
# Play result video immediately # Play result video immediately
logger.info("No match video playing, playing result video immediately") logger.info("No match video playing, playing result video immediately")
self._play_result_video(result_video_info) self._play_result_video(result_video_info)
except Exception as e: except Exception as e:
logger.error(f"Failed to handle PLAY_VIDEO_RESULT: {e}") logger.error(f"Failed to handle PLAY_VIDEO_RESULTS: {e}")
def _find_result_video_file(self, match_id: int, result: str) -> Optional[Path]: def _find_result_video_file(self, match_id: int, result: str) -> Optional[Path]:
"""Find the result video file ({RESULT}.mp4) from the match ZIP""" """Find the result video file ({RESULT}.mp4) from the match ZIP"""
...@@ -3753,7 +4042,7 @@ class QtVideoPlayer(QObject): ...@@ -3753,7 +4042,7 @@ class QtVideoPlayer(QObject):
logger.error(f"Failed to queue result video: {e}") logger.error(f"Failed to queue result video: {e}")
def _play_result_video(self, result_video_info: Dict[str, Any]): def _play_result_video(self, result_video_info: Dict[str, Any]):
"""Play result video immediately""" """Play result video with results overlay template"""
try: try:
fixture_id = result_video_info['fixture_id'] fixture_id = result_video_info['fixture_id']
match_id = result_video_info['match_id'] match_id = result_video_info['match_id']
...@@ -3763,7 +4052,7 @@ class QtVideoPlayer(QObject): ...@@ -3763,7 +4052,7 @@ class QtVideoPlayer(QObject):
logger.info(f"Playing result video: {result}.mp4 for match {match_id}") logger.info(f"Playing result video: {result}.mp4 for match {match_id}")
# Use the same overlay as match video initially # Use results overlay template
overlay_data = { overlay_data = {
'match_id': match_id, 'match_id': match_id,
'fixture_id': fixture_id, 'fixture_id': fixture_id,
...@@ -3774,24 +4063,18 @@ class QtVideoPlayer(QObject): ...@@ -3774,24 +4063,18 @@ class QtVideoPlayer(QObject):
'is_result_video': True 'is_result_video': True
} }
# Play the result video # Play the result video with results overlay template
self.window.play_video( self.window.play_video(
video_path, video_path,
template_data=overlay_data, template_data=overlay_data,
template_name="match_video.html", # Start with match video template template_name="results.html", # Use results template
loop_data=None loop_data=None
) )
# Set up overlay switching timer (3 seconds before end) # Store result video info for end-of-video handling
if duration > 3: self.current_result_video_info = result_video_info
switch_time = duration - 3
logger.info(f"Scheduling overlay switch in {switch_time} seconds")
# Store result video info for overlay switching
self.current_result_video_info = result_video_info
# Schedule overlay switch logger.info(f"Result video started with results overlay template for result: {result}")
QTimer.singleShot(int(switch_time * 1000), self._switch_to_result_overlay)
except Exception as e: except Exception as e:
logger.error(f"Failed to play result video: {e}") logger.error(f"Failed to play result video: {e}")
...@@ -3830,28 +4113,32 @@ class QtVideoPlayer(QObject): ...@@ -3830,28 +4113,32 @@ class QtVideoPlayer(QObject):
try: try:
# TODO: Get from template configuration # TODO: Get from template configuration
# For now, return default result template # For now, return default result template
return "results" return "results.html"
except Exception as e: except Exception as e:
logger.error(f"Failed to get result template: {e}") logger.error(f"Failed to get result template: {e}")
return "results" return "results.html"
def _send_match_video_done_message(self): def _send_match_video_done_message(self, match_id=None, video_filename=None, fixture_id=None):
"""Send PLAY_VIDEO_MATCH_DONE message when match video finishes""" """Send PLAY_VIDEO_MATCH_DONE message when match video finishes"""
try: try:
if (self.current_match_id is not None and # Use provided parameters or fall back to instance attributes
self.current_match_video_filename is not None): current_match_id = match_id if match_id is not None else getattr(self, 'current_match_id', None)
current_video_filename = video_filename if video_filename is not None else getattr(self, 'current_match_video_filename', None)
current_fixture_id = fixture_id if fixture_id is not None else getattr(self, 'current_fixture_id', None)
if (current_match_id is not None and current_video_filename is not None):
from ..core.message_bus import MessageBuilder from ..core.message_bus import MessageBuilder
done_message = MessageBuilder.play_video_match_done( done_message = MessageBuilder.play_video_match_done(
sender=self.name, sender=self.name,
match_id=self.current_match_id, match_id=current_match_id,
video_filename=self.current_match_video_filename, video_filename=current_video_filename,
fixture_id=self.current_fixture_id fixture_id=current_fixture_id
) )
self.message_bus.publish(done_message, broadcast=True) self.message_bus.publish(done_message, broadcast=True)
logger.info(f"Sent PLAY_VIDEO_MATCH_DONE for match {self.current_match_id}, video {self.current_match_video_filename}") logger.info(f"Sent PLAY_VIDEO_MATCH_DONE for match {current_match_id}, video {current_video_filename}")
# Check if there's a queued result video to play # Check if there's a queued result video to play
if hasattr(self, 'queued_result_video') and self.queued_result_video: if hasattr(self, 'queued_result_video') and self.queued_result_video:
...@@ -3863,6 +4150,28 @@ class QtVideoPlayer(QObject): ...@@ -3863,6 +4150,28 @@ class QtVideoPlayer(QObject):
except Exception as e: except Exception as e:
logger.error(f"Failed to send match video done message: {e}") logger.error(f"Failed to send match video done message: {e}")
def _send_result_video_done_message(self, result_info=None):
"""Send PLAY_VIDEO_RESULT_DONE message when result video finishes"""
try:
# Use provided result_info or fall back to instance attribute
current_result_info = result_info if result_info is not None else getattr(self, 'current_result_video_info', None)
if current_result_info:
from ..core.message_bus import MessageBuilder
done_message = MessageBuilder.play_video_result_done(
sender=self.name,
fixture_id=current_result_info.get('fixture_id'),
match_id=current_result_info.get('match_id'),
result=current_result_info.get('result')
)
self.message_bus.publish(done_message, broadcast=True)
logger.info(f"Sent PLAY_VIDEO_RESULT_DONE for fixture {current_result_info.get('fixture_id')}, match {current_result_info.get('match_id')}, result {current_result_info.get('result')}")
except Exception as e:
logger.error(f"Failed to send result video done message: {e}")
def _send_match_done_message(self): def _send_match_done_message(self):
"""Send MATCH_DONE message when result video is 3 seconds from ending""" """Send MATCH_DONE message when result video is 3 seconds from ending"""
try: try:
......
...@@ -36,16 +36,16 @@ ...@@ -36,16 +36,16 @@
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 20px; right: 20px;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 123, 255, 0.8);
color: white; color: white;
padding: 10px 15px; padding: 12px 18px;
border-radius: 8px; border-radius: 8px;
font-size: 24px; font-size: 32px;
font-weight: bold; font-weight: bold;
font-family: 'Courier New', monospace; font-family: 'Courier New', monospace;
border: 2px solid rgba(255, 255, 255, 0.3); border: 2px solid rgba(255, 255, 255, 0.3);
z-index: 1000; z-index: 1000;
min-width: 120px; min-width: 140px;
text-align: center; text-align: center;
} }
...@@ -55,25 +55,25 @@ ...@@ -55,25 +55,25 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: rgba(0, 0, 0, 0.85); background: rgba(0, 123, 255, 0.8);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-top: 2px solid rgba(255, 255, 255, 0.2); border-top: 2px solid rgba(255, 255, 255, 0.3);
padding: 15px; padding: 20px;
z-index: 1000; z-index: 1000;
} }
.fighter-names { .fighter-names {
color: white; color: white;
font-size: 28px; font-size: 36px;
font-weight: bold; font-weight: bold;
text-align: center; text-align: center;
margin-bottom: 5px; margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8); text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
} }
.venue-info { .venue-info {
color: rgba(255, 255, 255, 0.9); color: rgba(255, 255, 255, 0.95);
font-size: 20px; font-size: 28px;
text-align: center; text-align: center;
font-style: italic; font-style: italic;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.8);
...@@ -104,35 +104,137 @@ ...@@ -104,35 +104,137 @@
100% { transform: translate(-50%, -50%) scale(1); } 100% { transform: translate(-50%, -50%) scale(1); }
} }
/* Entrance animation for fighter names */
.entrance-animation {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
font-size: 72px;
font-weight: bold;
text-align: center;
text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.9);
z-index: 2000;
opacity: 0;
animation: entranceFadeIn 1s ease-out forwards, entranceFadeOut 1s ease-in 5s forwards;
pointer-events: none;
}
@keyframes entranceFadeIn {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes entranceFadeOut {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
}
/* Fight! animation */
.fight-animation {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ff4444;
font-size: 96px;
font-weight: bold;
text-align: center;
text-shadow: 6px 6px 12px rgba(0, 0, 0, 0.9);
z-index: 2001;
opacity: 0;
animation: fightFadeIn 0.2s ease-out forwards, fightHold 0.8s ease-in 0.2s forwards, fightFadeOut 0.5s ease-in 1.0s forwards;
}
@keyframes fightFadeIn {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.5);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
}
@keyframes fightHold {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
}
@keyframes fightFadeOut {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.2);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.8);
}
}
/* Responsive design */ /* Responsive design */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.fighter-names { .fighter-names {
font-size: 24px; font-size: 30px;
} }
.venue-info { .venue-info {
font-size: 18px; font-size: 24px;
} }
.timer-container { .timer-container {
font-size: 20px; font-size: 28px;
padding: 8px 12px; padding: 10px 14px;
min-width: 130px;
}
.entrance-animation {
font-size: 60px;
}
.fight-animation {
font-size: 80px;
} }
} }
@media (max-width: 800px) { @media (max-width: 800px) {
.fighter-names { .fighter-names {
font-size: 20px; font-size: 24px;
} }
.venue-info { .venue-info {
font-size: 16px; font-size: 20px;
} }
.timer-container { .timer-container {
font-size: 18px; font-size: 24px;
padding: 6px 10px; padding: 8px 12px;
top: 10px; top: 10px;
right: 10px; right: 10px;
min-width: 120px;
} }
.info-bar { .info-bar {
padding: 10px; padding: 15px;
}
.entrance-animation {
font-size: 48px;
}
.fight-animation {
font-size: 64px;
} }
} }
</style> </style>
...@@ -143,6 +245,16 @@ ...@@ -143,6 +245,16 @@
00:00 00:00
</div> </div>
<!-- Entrance animation for fighter names -->
<div class="entrance-animation" id="entranceAnimation" style="display: none;">
<div id="entranceFighterNames">Fighter 1 vs Fighter 2</div>
</div>
<!-- Fight! animation -->
<div class="fight-animation" id="fightAnimation" style="display: none;">
<div id="fightText">Fight!</div>
</div>
<!-- Result indicator (shown briefly at end) --> <!-- Result indicator (shown briefly at end) -->
<div class="result-indicator" id="resultIndicator"> <div class="result-indicator" id="resultIndicator">
<div id="resultText">RESULT</div> <div id="resultText">RESULT</div>
...@@ -155,13 +267,58 @@ ...@@ -155,13 +267,58 @@
</div> </div>
<script> <script>
// Global variables // Global variables for overlay data handling
let overlayData = {}; let overlayData = {};
let fixturesData = null;
let matchData = null; let matchData = null;
let videoDuration = 0; let videoDuration = 0;
let timerInterval = null; let timerInterval = null;
let currentTime = 0; let currentTime = 0;
let webServerBaseUrl = 'http://127.0.0.1:5001'; let webServerBaseUrl = 'http://127.0.0.1:5001';
let webServerUrlReceived = false;
let entranceAnimationShown = false; // Flag to prevent re-showing entrance animation
// Apply console.log override immediately with buffering
(function() {
var originalConsoleLog = console.log;
var messageBuffer = [];
console.log = function(...args) {
var message = args.map(String).join(' ');
if (window.overlay && window.overlay.log) {
window.overlay.log('[LOG] ' + message);
} else {
messageBuffer.push('[LOG] ' + message);
}
originalConsoleLog.apply(console, args);
};
// Function to flush buffer when overlay becomes available
window.flushConsoleBuffer = function() {
if (window.overlay && window.overlay.log) {
messageBuffer.forEach(function(msg) {
window.overlay.log(msg);
});
messageBuffer = [];
}
};
// Check periodically for overlay availability
var checkInterval = setInterval(function() {
if (window.overlay && window.overlay.log) {
window.flushConsoleBuffer();
clearInterval(checkInterval);
}
}, 50);
// Clear interval after 5 seconds to avoid infinite polling
setTimeout(function() {
clearInterval(checkInterval);
}, 5000);
})();
// Test console override
console.log('TEST: Console override applied and buffering');
// Function to update overlay data (called by Qt WebChannel) // Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) { function updateOverlayData(data) {
...@@ -171,13 +328,20 @@ ...@@ -171,13 +328,20 @@
// Update web server base URL if provided // Update web server base URL if provided
if (data && data.webServerBaseUrl) { if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl; webServerBaseUrl = data.webServerBaseUrl;
webServerUrlReceived = true;
console.log('Updated web server base URL:', webServerBaseUrl); console.log('Updated web server base URL:', webServerBaseUrl);
} }
// Extract match data // Extract match data
if (data && data.match) { if (data && data.match) {
const hadMatchData = !!matchData;
matchData = data.match; matchData = data.match;
updateMatchInfo(); updateMatchInfo();
// Only show entrance animation if not already shown and this is first match data
if (!hadMatchData && !entranceAnimationShown) {
console.log('DEBUG: [MATCH_VIDEO] Match data received via WebChannel, showing entrance animation');
showEntranceAnimation();
}
} }
// Extract video duration if provided // Extract video duration if provided
...@@ -271,63 +435,231 @@ ...@@ -271,63 +435,231 @@
}, 5000); }, 5000);
} }
// Fetch match data from API // Fetch fixture data using WebChannel instead of API fetch
async function fetchMatchData(matchId) { async function fetchFixtureData() {
try { try {
console.log('Fetching match data for ID:', matchId); console.log('DEBUG: [MATCH_VIDEO] Attempting to get fixture data from WebChannel');
console.log('DEBUG: [MATCH_VIDEO] window.overlay exists:', !!window.overlay);
const response = await fetch(`${webServerBaseUrl}/api/matches/${matchId}`); console.log('DEBUG: [MATCH_VIDEO] window.overlay.getFixtureData exists:', !!(window.overlay && window.overlay.getFixtureData));
const data = await response.json();
if (window.overlay && window.overlay.getFixtureData) {
if (data.success && data.match) { // Get fixture data from Qt WebChannel (returns a Promise)
matchData = data.match; const fixtureJson = await window.overlay.getFixtureData();
updateMatchInfo(); console.log('DEBUG: [MATCH_VIDEO] Received fixture data from WebChannel:', fixtureJson);
console.log('Fetched match data:', matchData);
if (fixtureJson) {
try {
fixturesData = JSON.parse(fixtureJson);
console.log('DEBUG: [MATCH_VIDEO] Parsed fixture data:', fixturesData);
console.log('DEBUG: [MATCH_VIDEO] fixturesData.length:', fixturesData ? fixturesData.length : 'null');
if (fixturesData && fixturesData.length > 0) {
console.log('DEBUG: [MATCH_VIDEO] WebChannel returned fixture data, calling updateMatchInfo()');
updateMatchInfoFromFixtures();
} else {
console.log('DEBUG: [MATCH_VIDEO] WebChannel returned empty fixture data');
}
} catch (parseError) {
console.log('DEBUG: [MATCH_VIDEO] Failed to parse fixture data from WebChannel:', parseError);
}
} else {
console.log('DEBUG: [MATCH_VIDEO] WebChannel returned null/empty fixture data');
}
} else { } else {
console.warn('Failed to fetch match data:', data); console.log('DEBUG: [MATCH_VIDEO] WebChannel overlay.getFixtureData not available');
console.log('DEBUG: [MATCH_VIDEO] window object keys:', Object.keys(window));
if (window.overlay) {
console.log('DEBUG: [MATCH_VIDEO] window.overlay keys:', Object.keys(window.overlay));
}
} }
} catch (error) { } catch (error) {
console.error('Error fetching match data:', error); console.log('DEBUG: [MATCH_VIDEO] Exception caught in fetchFixtureData');
console.log('DEBUG: [MATCH_VIDEO] Error message =', error.message);
console.log('DEBUG: [MATCH_VIDEO] Error stack =', error.stack);
} }
} }
// Initialize when DOM is loaded // Update match info from fixtures data
document.addEventListener('DOMContentLoaded', function() { function updateMatchInfoFromFixtures() {
console.log('Match video overlay initialized'); if (!fixturesData || fixturesData.length === 0) return;
// Start timer immediately (will be synced with video) // Get the first match (should be the current match being played)
startTimer(); const currentMatch = fixturesData[0];
console.log('DEBUG: [MATCH_VIDEO] Using match data from fixtures:', currentMatch);
// If we have match data from overlay, use it if (currentMatch) {
if (overlayData && overlayData.match) { const hadMatchData = !!matchData;
matchData = currentMatch;
updateMatchInfo(); updateMatchInfo();
} else if (overlayData && overlayData.match_id) {
// Try to fetch match data // Show entrance animation only on first match data load, not on periodic refreshes
fetchMatchData(overlayData.match_id); if (!hadMatchData && !entranceAnimationShown) {
console.log('DEBUG: [MATCH_VIDEO] First match data received, showing entrance animation');
showEntranceAnimation();
}
} }
}); }
// Show entrance animation with fighter names
function showEntranceAnimation() {
if (!matchData) return;
const entranceElement = document.getElementById('entranceAnimation');
const entranceText = document.getElementById('entranceFighterNames');
// Get fighter names
const fighter1 = matchData.fighter1_township || matchData.fighter1 || 'Fighter 1';
const fighter2 = matchData.fighter2_township || matchData.fighter2 || 'Fighter 2';
entranceText.textContent = `${fighter1} vs ${fighter2}`;
entranceElement.style.display = 'block';
// Mark that entrance animation has been shown
entranceAnimationShown = true;
console.log('DEBUG: [MATCH_VIDEO] Showing entrance animation for:', `${fighter1} vs ${fighter2}`);
// Animation will automatically hide after 6 seconds (5s display + 1s fade out)
setTimeout(() => {
entranceElement.style.display = 'none';
console.log('DEBUG: [MATCH_VIDEO] Entrance animation completed');
// Start the "Fight!" animation after entrance animation completes
setTimeout(() => {
showFightAnimation();
}, 100); // Small delay to ensure entrance animation is fully hidden
}, 6000);
}
// Show "Fight!" animation
function showFightAnimation() {
const fightElement = document.getElementById('fightAnimation');
const fightText = document.getElementById('fightText');
fightText.textContent = 'Fight!';
fightElement.style.display = 'block';
console.log('DEBUG: [MATCH_VIDEO] Showing Fight! animation');
// Qt WebChannel initialization // Animation will automatically hide after 1.5 seconds (0.2s fade in + 0.8s display + 0.5s fade out)
if (typeof QWebChannel !== 'undefined') { setTimeout(() => {
new QWebChannel(qt.webChannelTransport, function(channel) { fightElement.style.display = 'none';
console.log('WebChannel initialized for match video overlay'); console.log('DEBUG: [MATCH_VIDEO] Fight! animation completed');
}, 1500);
}
// Connect to overlay object if available // Setup WebChannel communication
if (channel.objects.overlay) { function setupWebChannel() {
channel.objects.overlay.dataChanged.connect(function(data) { // Check if WebChannel is already set up by overlay.js
updateOverlayData(data); if (window.overlay) {
console.log('DEBUG: [MATCH_VIDEO] WebChannel already set up by overlay.js');
// Test WebChannel
if (window.overlay && window.overlay.log) {
window.overlay.log('TEST: WebChannel connection successful');
}
// Listen for data updates from Python
if (window.overlay.dataUpdated) {
window.overlay.dataUpdated.connect(function(data) {
console.log('DEBUG: [MATCH_VIDEO] Received data update from Python:', data);
if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl;
webServerUrlReceived = true;
console.log('DEBUG: [MATCH_VIDEO] Web server base URL updated to:', webServerBaseUrl);
// If we were waiting for the URL, start fetching data now
if (document.readyState === 'complete' || document.readyState === 'interactive') {
console.log('DEBUG: [MATCH_VIDEO] Document ready, starting data fetch after URL update');
fetchFixtureData();
}
}
if (data && data.debugMode) {
console.log('DEBUG: [MATCH_VIDEO] Debug mode enabled');
}
if (data && data.timer_update) {
console.log('DEBUG: [MATCH_VIDEO] Timer update received:', data.timer_update);
}
}); });
}
return;
}
// Get initial data // Fallback: setup WebChannel if overlay.js didn't do it
if (channel.objects.overlay.getCurrentData) { if (typeof qt !== 'undefined' && qt.webChannelTransport) {
channel.objects.overlay.getCurrentData(function(data) { try {
updateOverlayData(data); new QWebChannel(qt.webChannelTransport, function(channel) {
}); console.log('DEBUG: [MATCH_VIDEO] WebChannel connected successfully (fallback)');
}
// Connect to overlay object
window.overlay = channel.objects.overlay;
// Listen for data updates from Python
if (window.overlay && window.overlay.dataUpdated) {
window.overlay.dataUpdated.connect(function(data) {
console.log('DEBUG: [MATCH_VIDEO] Received data update from Python:', data);
if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl;
webServerUrlReceived = true;
console.log('DEBUG: [MATCH_VIDEO] Web server base URL updated to:', webServerBaseUrl);
}
});
} else {
console.log('DEBUG: [MATCH_VIDEO] Overlay object not available in WebChannel');
}
});
} catch (e) {
console.log('DEBUG: [MATCH_VIDEO] Failed to setup WebChannel:', e);
} }
}); } else {
console.log('DEBUG: [MATCH_VIDEO] WebChannel not available, using default webServerBaseUrl');
}
} }
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('Match video overlay initialized');
// Setup WebChannel first
setupWebChannel();
// Start timer immediately (will be synced with video)
startTimer();
// Wait briefly for WebChannel to connect and potentially receive webServerBaseUrl
setTimeout(() => {
if (!webServerUrlReceived) {
console.log('DEBUG: [MATCH_VIDEO] WebServerBaseUrl not received via WebChannel, proceeding with WebChannel data fetch');
}
// Fetch fixture data directly from WebChannel
console.log('DEBUG: [MATCH_VIDEO] Starting fixture data fetch via WebChannel');
fetchFixtureData();
// Set up periodic refresh every 30 seconds
setTimeout(function refreshData() {
console.log('DEBUG: [MATCH_VIDEO] Periodic refresh: fetching updated fixture data');
fetchFixtureData();
setTimeout(refreshData, 30000);
}, 30000);
// Fallback: Check if we have match data and show animation after 2 seconds
setTimeout(() => {
if (matchData && !entranceAnimationShown) {
console.log('DEBUG: [MATCH_VIDEO] Fallback: Match data available, showing entrance animation');
showEntranceAnimation();
}
}, 2000);
// Show no matches if no data after 5 seconds total
setTimeout(() => {
if (!matchData) {
console.log('DEBUG: [MATCH_VIDEO] No match data received after 5 seconds');
}
}, 5000);
}, 50); // Wait 50ms for WebChannel setup
});
// Cleanup on page unload // Cleanup on page unload
window.addEventListener('beforeunload', function() { window.addEventListener('beforeunload', function() {
stopTimer(); stopTimer();
......
...@@ -42,35 +42,140 @@ ...@@ -42,35 +42,140 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.results-panel { .results-panel {
background: rgba(0, 123, 255, 0.40); background: rgba(0, 123, 255, 0.85);
border-radius: 20px; border-radius: 20px;
padding: 50px; padding: 30px;
min-width: 600px; max-width: 90%;
min-height: 400px; max-height: 85%;
display: flex; overflow: visible;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.1);
opacity: 0; opacity: 0;
transform: scale(0.5) rotate(-10deg); animation: fadeInScale 1s ease-out forwards;
animation: dramaticEntrance 1.5s ease-out forwards; padding-bottom: 50px;
display: none; /* Initially hidden until data is available */ display: none; /* Initially hidden until data is available */
} }
.results-title { .results-title {
color: white; color: white;
font-size: 36px; font-size: 56px;
font-weight: bold; font-weight: bold;
text-align: center;
margin-bottom: 25px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.fighters-display {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 30px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5); }
opacity: 0;
animation: titleBounce 2s ease-out 0.5s forwards; .fighter-names {
font-size: 48px;
font-weight: bold;
color: #ffffff;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.6);
letter-spacing: 2px;
text-align: center;
line-height: 1.2;
}
.main-result-display {
text-align: center;
margin-bottom: 20px;
}
.main-result-text {
font-size: 72px;
font-weight: bold;
color: #ffffff;
text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.6);
letter-spacing: 4px;
text-align: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
display: inline-block;
min-width: 300px;
}
.under-over-display {
text-align: center;
margin-bottom: 40px;
}
.under-over-text {
font-size: 48px;
font-weight: bold;
color: #e6f3ff;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.6);
letter-spacing: 2px;
text-align: center;
background: rgba(40, 167, 69, 0.3);
border-radius: 10px;
padding: 15px 30px;
border: 2px solid rgba(40, 167, 69, 0.5);
display: inline-block;
min-width: 200px;
}
.under-over-display.over .under-over-text {
background: rgba(253, 126, 20, 0.3);
border-color: rgba(253, 126, 20, 0.5);
}
.winning-outcomes-section {
width: 100%;
}
.outcomes-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
max-height: 400px;
overflow-y: auto;
padding: 10px;
}
.outcome-card {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 20px;
text-align: center;
backdrop-filter: blur(5px);
transition: all 0.3s ease;
}
.outcome-card:hover {
transform: scale(1.05);
border-color: rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.15);
}
.outcome-name {
font-size: 24px;
font-weight: bold;
color: #ffffff;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.outcome-amount {
font-size: 20px;
color: #e6f3ff;
}
.no-outcomes-message {
color: rgba(255, 255, 255, 0.7);
font-size: 18px;
text-align: center;
padding: 20px;
grid-column: 1 / -1;
} }
.outcome-display { .outcome-display {
...@@ -227,17 +332,14 @@ ...@@ -227,17 +332,14 @@
} }
/* Animations */ /* Animations */
@keyframes dramaticEntrance { @keyframes fadeInScale {
0% { 0% {
opacity: 0; opacity: 0;
transform: scale(0.5) rotate(-10deg); transform: scale(0.8);
}
50% {
transform: scale(1.1) rotate(2deg);
} }
100% { 100% {
opacity: 1; opacity: 1;
transform: scale(1) rotate(0deg); transform: scale(1);
} }
} }
...@@ -414,32 +516,152 @@ ...@@ -414,32 +516,152 @@
content: ''; content: '';
animation: loadingDots 1.5s infinite; animation: loadingDots 1.5s infinite;
} }
/* Match Info Section Styles */
.match-info-section {
text-align: center;
margin-bottom: 30px;
width: 100%;
}
.match-title {
color: white;
font-size: 48px;
font-weight: bold;
text-align: center;
margin-bottom: 40px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5);
opacity: 0;
animation: titleBounce 2s ease-out 0.5s forwards;
}
.fighters-display {
margin: 30px 0;
opacity: 0;
animation: fadeInUp 1s ease-out 1s forwards;
}
.fighter-names {
font-size: 64px;
font-weight: bold;
color: #ffffff;
text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.6);
letter-spacing: 2px;
text-align: center;
line-height: 1.2;
}
.venue-info {
color: rgba(255, 255, 255, 0.9);
font-size: 24px;
font-style: italic;
margin-top: 20px;
opacity: 0;
animation: fadeInUp 1s ease-out 1.5s forwards;
}
/* Winning Bets Section Styles */
.winning-bets-section {
width: 100%;
margin-top: 20px;
}
.bets-title {
color: white;
font-size: 36px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5);
opacity: 0;
animation: titleBounce 2s ease-out 5.5s forwards;
}
.bets-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
max-height: 300px;
overflow-y: auto;
padding: 10px;
opacity: 0;
animation: fadeInUp 1s ease-out 6s forwards;
}
.bet-card {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 15px;
text-align: center;
backdrop-filter: blur(5px);
transition: all 0.3s ease;
}
.bet-card:hover {
transform: scale(1.05);
border-color: rgba(255, 255, 255, 0.4);
background: rgba(255, 255, 255, 0.15);
}
.bet-outcome {
font-size: 18px;
font-weight: bold;
color: #ffffff;
margin-bottom: 8px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.bet-amount {
font-size: 16px;
color: #e6f3ff;
}
.bet-win-amount {
font-size: 20px;
font-weight: bold;
color: #ffff99;
margin-top: 5px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
}
.no-bets-message {
color: rgba(255, 255, 255, 0.7);
font-size: 18px;
text-align: center;
padding: 20px;
grid-column: 1 / -1;
}
</style> </style>
</head> </head>
<body> <body>
<div class="overlay-container"> <div class="overlay-container">
<!-- Always show results panel with default content -->
<div class="results-panel" id="resultsPanel" style="display: flex;"> <div class="results-panel" id="resultsPanel" style="display: flex;">
<div class="celebration-effects" id="celebrationEffects"> <!-- Title -->
<!-- Confetti will be generated here --> <div class="results-title">RESULTS</div>
<!-- Fighters Display -->
<div class="fighters-display" id="fightersDisplay">
<div class="fighter-names" id="fighterNames">
<span id="fighter1">Fighter 1</span> VS <span id="fighter2">Fighter 2</span>
</div>
</div> </div>
<div class="flash-effect"></div>
<!-- Main Result -->
<div class="results-title">MATCH RESULTS</div> <div class="main-result-display" id="mainResultDisplay">
<div class="main-result-text" id="mainResultText">WIN1</div>
<div class="outcome-display" id="outcomeDisplay">
<div class="outcome-text" id="outcomeText">READY</div>
</div> </div>
<div class="match-info" id="matchInfo"> <!-- Under/Over Result -->
<div class="match-number">Match #<span id="matchNumber">--</span></div> <div class="under-over-display" id="underOverDisplay">
<div class="fighters"> <div class="under-over-text" id="underOverText">UNDER</div>
<span id="fighter1">Fighter 1</span> vs <span id="fighter2">Fighter 2</span> </div>
</div>
<div class="winning-fighter" id="winningFighter" style="display: none;"> <!-- Winning Outcomes Grid -->
Winner: <span id="winnerName"></span> <div class="winning-outcomes-section">
<div class="outcomes-grid" id="outcomesGrid">
<!-- Winning outcomes will be populated here -->
</div> </div>
<div class="venue">@ <span id="venue">Venue</span></div>
</div> </div>
</div> </div>
</div> </div>
...@@ -447,8 +669,11 @@ ...@@ -447,8 +669,11 @@
<script> <script>
// Global variables for overlay data handling // Global variables for overlay data handling
let overlayData = {}; let overlayData = {};
let currentOutcome = 'WIN1'; let currentMainResult = 'WIN1';
let currentUnderOverResult = 'UNDER';
let currentMatch = null; let currentMatch = null;
let winningOutcomes = [];
let animationStarted = false;
// Outcome categories for styling // Outcome categories for styling
const outcomeCategories = { const outcomeCategories = {
...@@ -471,31 +696,100 @@ ...@@ -471,31 +696,100 @@
function updateOverlayData(data) { function updateOverlayData(data) {
console.log('Received overlay data:', data); console.log('Received overlay data:', data);
overlayData = data || {}; overlayData = data || {};
// Only update if we have valid data // Only update if we have valid data
if (data && (data.outcome || data.result)) { if (data && (data.outcome || data.result)) {
if (data.outcome) { let result = data.outcome || data.result;
currentOutcome = data.outcome; console.log('Processing result:', result);
} else if (data.result) {
currentOutcome = data.result; // Always treat the main result as the primary outcome
currentMainResult = result;
// Check if under/over result is provided separately
if (data.under_over_result) {
currentUnderOverResult = data.under_over_result;
console.log('Under/over result provided separately:', currentUnderOverResult);
} else {
// Fallback: determine if main result is under/over
if (result === 'UNDER' || result === 'OVER') {
currentUnderOverResult = result;
currentMainResult = null; // No separate main result when under/over is the main result
} else {
currentUnderOverResult = null; // No under/over result
}
} }
if (data.match) { if (data.match) {
currentMatch = data.match; currentMatch = data.match;
} }
if (data.winning_fighter) { if (data.match_id) {
winningFighter = data.winning_fighter; // Fetch winning outcomes for this match
fetchWinningOutcomes(data.match_id);
} }
// Show results panel and hide loading state // Start the animation sequence
showResultsPanel(); startResultsAnimation();
updateResultsDisplay();
} else { } else {
// No valid data, show loading state // No valid data, show loading state
showLoadingState(); showLoadingState();
} }
} }
// Start the results animation sequence
function startResultsAnimation() {
if (animationStarted) return;
animationStarted = true;
// Show results panel immediately
showResultsPanel();
// Update fighters display
updateFightersDisplay();
// Update main result display
updateMainResultDisplay();
// Update under/over result display
updateUnderOverResultDisplay();
// Update winning outcomes display
updateWinningOutcomesDisplay();
}
// Fetch winning outcomes for the match
function fetchWinningOutcomes(matchId) {
console.log('Fetching winning outcomes for match:', matchId);
// Use Qt WebChannel to request winning outcomes data
if (window.overlay && window.overlay.getWinningOutcomes) {
try {
const outcomesJson = window.overlay.getWinningOutcomes(matchId);
const outcomesData = JSON.parse(outcomesJson);
console.log('Received winning outcomes:', outcomesData);
winningOutcomes = outcomesData || [];
updateWinningOutcomesDisplay();
} catch (error) {
console.error('Failed to get winning outcomes:', error);
// Fallback: show sample data for testing
winningOutcomes = [
{ outcome: 'WIN1', amount: 125.00 },
{ outcome: 'OVER', amount: 87.50 },
{ outcome: 'KO1', amount: 95.00 }
];
updateWinningOutcomesDisplay();
}
} else {
console.warn('Qt WebChannel not available for fetching winning outcomes');
// Fallback: show sample data for testing
winningOutcomes = [
{ outcome: 'WIN1', amount: 125.00 },
{ outcome: 'OVER', amount: 87.50 },
{ outcome: 'KO1', amount: 95.00 }
];
updateWinningOutcomesDisplay();
}
}
// Show results panel (always visible now) // Show results panel (always visible now)
function showResultsPanel() { function showResultsPanel() {
...@@ -503,66 +797,83 @@ ...@@ -503,66 +797,83 @@
resultsPanel.style.display = 'flex'; resultsPanel.style.display = 'flex';
} }
// Update the results display // Update fighters display
function updateResultsDisplay() { function updateFightersDisplay() {
// Don't update if no valid outcome data if (!currentMatch) {
if (!currentOutcome) {
showLoadingState();
return; return;
} }
const outcomeText = document.getElementById('outcomeText');
const resultsPanel = document.getElementById('resultsPanel');
const matchNumber = document.getElementById('matchNumber');
const fighter1 = document.getElementById('fighter1'); const fighter1 = document.getElementById('fighter1');
const fighter2 = document.getElementById('fighter2'); const fighter2 = document.getElementById('fighter2');
const venue = document.getElementById('venue');
const winningFighterDiv = document.getElementById('winningFighter'); // Update fighter names
const winnerName = document.getElementById('winnerName');
// Update outcome text
outcomeText.textContent = currentOutcome;
// Apply outcome-specific styling
const category = outcomeCategories[currentOutcome] || 'default';
resultsPanel.className = `results-panel outcome-${category}`;
// Update match info if available
if (currentMatch) { if (currentMatch) {
matchNumber.textContent = currentMatch.match_number || ''; fighter1.textContent = currentMatch.fighter1_township || 'Fighter 1';
fighter1.textContent = currentMatch.fighter1_township || ''; fighter2.textContent = currentMatch.fighter2_township || 'Fighter 2';
fighter2.textContent = currentMatch.fighter2_township || '';
venue.textContent = currentMatch.venue_kampala_township || '';
} }
}
// Update winning fighter if available
if (overlayData.winning_fighter) { // Update main result display
winnerName.textContent = overlayData.winning_fighter; function updateMainResultDisplay() {
winningFighterDiv.style.display = 'block'; const mainResultText = document.getElementById('mainResultText');
const mainResultDisplay = document.getElementById('mainResultDisplay');
if (currentMainResult) {
mainResultText.textContent = currentMainResult;
mainResultDisplay.style.display = 'block';
} else { } else {
// Try to determine winner from outcome and fighter names mainResultDisplay.style.display = 'none';
if (currentOutcome === 'WIN1' && currentMatch && currentMatch.fighter1_township) {
winnerName.textContent = currentMatch.fighter1_township;
winningFighterDiv.style.display = 'block';
} else if (currentOutcome === 'WIN2' && currentMatch && currentMatch.fighter2_township) {
winnerName.textContent = currentMatch.fighter2_township;
winningFighterDiv.style.display = 'block';
} else if (currentOutcome.includes('1') && currentMatch && currentMatch.fighter1_township) {
winnerName.textContent = currentMatch.fighter1_township;
winningFighterDiv.style.display = 'block';
} else if (currentOutcome.includes('2') && currentMatch && currentMatch.fighter2_township) {
winnerName.textContent = currentMatch.fighter2_township;
winningFighterDiv.style.display = 'block';
} else {
winningFighterDiv.style.display = 'none';
}
} }
}
// Generate confetti effects
generateConfetti(); // Update under/over result display
function updateUnderOverResultDisplay() {
// Restart animations const underOverText = document.getElementById('underOverText');
restartAnimations(); const underOverDisplay = document.getElementById('underOverDisplay');
if (currentUnderOverResult) {
underOverText.textContent = currentUnderOverResult;
underOverDisplay.className = `under-over-display ${currentUnderOverResult.toLowerCase()}`;
underOverDisplay.style.display = 'block';
} else {
underOverDisplay.style.display = 'none';
}
}
// Update winning outcomes display with staggered animation
function updateWinningOutcomesDisplay() {
const outcomesGrid = document.getElementById('outcomesGrid');
outcomesGrid.innerHTML = '';
if (!winningOutcomes || winningOutcomes.length === 0) {
const noOutcomesDiv = document.createElement('div');
noOutcomesDiv.className = 'no-outcomes-message';
noOutcomesDiv.textContent = 'No winning outcomes for this match';
outcomesGrid.appendChild(noOutcomesDiv);
return;
}
// Create outcome cards for each winning outcome with staggered animation
winningOutcomes.forEach((outcome, index) => {
const outcomeCard = document.createElement('div');
outcomeCard.className = 'outcome-card';
outcomeCard.style.opacity = '0';
outcomeCard.style.transform = 'translateY(20px)';
outcomeCard.innerHTML = `
<div class="outcome-name">${outcome.outcome || 'Unknown'}</div>
<div class="outcome-amount">$${outcome.amount ? outcome.amount.toFixed(2) : '0.00'}</div>
`;
outcomesGrid.appendChild(outcomeCard);
// Animate in with delay (staggered by 200ms)
setTimeout(() => {
outcomeCard.style.transition = 'all 0.5s ease-out';
outcomeCard.style.opacity = '1';
outcomeCard.style.transform = 'translateY(0)';
}, index * 200);
});
} }
// Generate confetti effects // Generate confetti effects
......
...@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent): ...@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent):
def _create_server(self): def _create_server(self):
"""Create HTTP/HTTPS server with SocketIO support""" """Create HTTP/HTTPS server with SocketIO support"""
try: try:
from werkzeug.serving import make_server
protocol = "HTTP" protocol = "HTTP"
if self.settings.enable_ssl: if self.settings.enable_ssl:
protocol = "HTTPS" protocol = "HTTPS"
logger.info("SSL enabled - SocketIO server will use HTTPS") logger.info("SSL enabled - server will use HTTPS")
# Get SSL certificate paths
from ..config.settings import get_user_data_dir
cert_path, key_path = get_ssl_certificate_paths(get_user_data_dir())
if cert_path and key_path:
# Create SSL context for HTTPS
self.ssl_context = create_ssl_context(cert_path, key_path)
if not self.ssl_context:
logger.warning("SSL context creation failed, falling back to HTTP")
self.ssl_context = None
protocol = "HTTP"
else:
logger.warning("SSL certificate files not available, falling back to HTTP")
self.ssl_context = None
protocol = "HTTP"
# Create WSGI server that can be shutdown
if self.socketio:
# For SocketIO, try to use the SocketIO WSGI app
try:
wsgi_app = self.socketio.WSGIApp(self.app)
except AttributeError:
# Fallback for older SocketIO versions or different implementations
logger.warning("SocketIO WSGIApp not available, falling back to standard Flask app")
wsgi_app = self.app
self.socketio = None # Disable SocketIO since we can't use it
else:
wsgi_app = self.app
self.server = make_server(
host=self.settings.host,
port=self.settings.port,
app=wsgi_app,
threaded=True,
ssl_context=self.ssl_context
)
logger.info(f"{protocol} server with SocketIO created on {self.settings.host}:{self.settings.port}") logger.info(f"{protocol} server created on {self.settings.host}:{self.settings.port}")
if self.settings.enable_ssl: if self.settings.enable_ssl and self.ssl_context:
logger.info("⚠️ Using self-signed certificate - browsers will show security warning") logger.info("⚠️ Using self-signed certificate - browsers will show security warning")
logger.info(" You can safely proceed by accepting the certificate") logger.info(" You can safely proceed by accepting the certificate")
...@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent): ...@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent):
socketio_status = "with SocketIO" if self.socketio else "without SocketIO" socketio_status = "with SocketIO" if self.socketio else "without SocketIO"
logger.info(f"Starting {protocol} server {socketio_status} on {self.settings.host}:{self.settings.port}") logger.info(f"Starting {protocol} server {socketio_status} on {self.settings.host}:{self.settings.port}")
if self.socketio: if self.server:
# Run SocketIO server # Use the shutdown-capable server
self.socketio.run( # serve_forever() will block until shutdown() is called
self.app, self.server.serve_forever()
host=self.settings.host, logger.info("HTTP server stopped")
port=self.settings.port,
debug=False,
use_reloader=False,
log_output=False
)
else: else:
# Run Flask server without SocketIO logger.error("Server not created, cannot start")
self.app.run( return
host=self.settings.host,
port=self.settings.port,
debug=False,
use_reloader=False
)
except Exception as e: except Exception as e:
if self.running: # Only log if not shutting down if self.running: # Only log if not shutting down
protocol = "HTTPS" if self.settings.enable_ssl else "HTTP" protocol = "HTTPS" if self.settings.enable_ssl else "HTTP"
logger.error(f"{protocol} server error: {e}") logger.error(f"{protocol} server error: {e}")
else:
# Expected during shutdown
logger.debug(f"Server stopped during shutdown: {e}")
def _setup_ssl_error_suppression(self): def _setup_ssl_error_suppression(self):
"""Setup logging filter to suppress expected SSL connection errors""" """Setup logging filter to suppress expected SSL connection errors"""
...@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent): ...@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent):
"""Shutdown web dashboard""" """Shutdown web dashboard"""
try: try:
logger.info("Shutting down WebDashboard...") logger.info("Shutting down WebDashboard...")
# Shutdown the HTTP server
if self.server: if self.server:
logger.info("Shutting down HTTP server...")
self.server.shutdown() self.server.shutdown()
logger.info("HTTP server shutdown initiated")
# Note: SocketIO connections will be closed when the server shuts down
# No explicit SocketIO shutdown needed as it's handled by the WSGI server
except Exception as e: except Exception as e:
logger.error(f"WebDashboard shutdown error: {e}") logger.error(f"WebDashboard shutdown error: {e}")
......
...@@ -410,17 +410,9 @@ def api_tokens(): ...@@ -410,17 +410,9 @@ def api_tokens():
def fixtures(): def fixtures():
"""Fixtures management page""" """Fixtures management page"""
try: try:
# Restrict cashier users from accessing fixtures page
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/fixtures.html', return render_template('dashboard/fixtures.html',
user=current_user, user=current_user,
page_title="Fixtures") page_title="Fixtures")
except Exception as e: except Exception as e:
logger.error(f"Fixtures page error: {e}") logger.error(f"Fixtures page error: {e}")
flash("Error loading fixtures", "error") flash("Error loading fixtures", "error")
...@@ -432,18 +424,10 @@ def fixtures(): ...@@ -432,18 +424,10 @@ def fixtures():
def fixture_details(fixture_id): def fixture_details(fixture_id):
"""Fixture details page showing all matches in the fixture""" """Fixture details page showing all matches in the fixture"""
try: try:
# Restrict cashier users from accessing fixture details page
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/fixture_details.html', return render_template('dashboard/fixture_details.html',
user=current_user, user=current_user,
fixture_id=fixture_id, fixture_id=fixture_id,
page_title=f"Fixture Details - Fixture #{fixture_id}") page_title=f"Fixture Details - Fixture #{fixture_id}")
except Exception as e: except Exception as e:
logger.error(f"Fixture details page error: {e}") logger.error(f"Fixture details page error: {e}")
flash("Error loading fixture details", "error") flash("Error loading fixture details", "error")
...@@ -455,19 +439,11 @@ def fixture_details(fixture_id): ...@@ -455,19 +439,11 @@ def fixture_details(fixture_id):
def match_details(match_id, fixture_id): def match_details(match_id, fixture_id):
"""Match details page showing match information and outcomes""" """Match details page showing match information and outcomes"""
try: try:
# Restrict cashier users from accessing match details page
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/match_details.html', return render_template('dashboard/match_details.html',
user=current_user, user=current_user,
match_id=match_id, match_id=match_id,
fixture_id=fixture_id, fixture_id=fixture_id,
page_title=f"Match Details - Match #{match_id}") page_title=f"Match Details - Match #{match_id}")
except Exception as e: except Exception as e:
logger.error(f"Match details page error: {e}") logger.error(f"Match details page error: {e}")
flash("Error loading match details", "error") flash("Error loading match details", "error")
...@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id): ...@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id):
'fighter1_township': match.fighter1_township, 'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township, 'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township, 'venue_kampala_township': match.venue_kampala_township,
'status': match.status 'status': match.status,
'result': match.result
} }
else: else:
detail_data['match'] = None detail_data['match'] = None
...@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id): ...@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id):
'fighter1_township': match.fighter1_township, 'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township, 'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township, 'venue_kampala_township': match.venue_kampala_township,
'status': match.status 'status': match.status,
'result': match.result
} }
else: else:
detail_data['match'] = None detail_data['match'] = None
......
...@@ -590,6 +590,7 @@ function displayBetDetails(bet) { ...@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr> <tr>
<td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br> <td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br>
<small class="text-muted">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</small> <small class="text-muted">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</small>
${detail.match && detail.match.result ? `<br><small class="text-info"><i class="fas fa-trophy me-1"></i>Result: ${detail.match.result}</small>` : ''}
</td> </td>
<td><span class="badge bg-primary">${detail.outcome}</span></td> <td><span class="badge bg-primary">${detail.outcome}</span></td>
<td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td> <td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td>
......
...@@ -590,6 +590,7 @@ function displayBetDetails(bet) { ...@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr> <tr>
<td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br> <td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br>
<small class="text-muted">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</small> <small class="text-muted">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</small>
${detail.match && detail.match.result ? `<br><small class="text-info"><i class="fas fa-trophy me-1"></i>Result: ${detail.match.result}</small>` : ''}
</td> </td>
<td><span class="badge bg-primary">${detail.outcome}</span></td> <td><span class="badge bg-primary">${detail.outcome}</span></td>
<td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td> <td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td>
......
...@@ -579,6 +579,7 @@ ...@@ -579,6 +579,7 @@
<div class="col-8"> <div class="col-8">
<h6 class="fw-bold mb-1">Match #${detail.match ? detail.match.match_number : 'Unknown'}</h6> <h6 class="fw-bold mb-1">Match #${detail.match ? detail.match.match_number : 'Unknown'}</h6>
<p class="text-muted small mb-1">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</p> <p class="text-muted small mb-1">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</p>
${detail.match && detail.match.result ? `<p class="text-info small mb-1"><i class="fas fa-trophy me-1"></i>Result: ${detail.match.result}</p>` : ''}
<span class="badge bg-primary">${detail.outcome}</span> <span class="badge bg-primary">${detail.outcome}</span>
</div> </div>
<div class="col-4 text-end"> <div class="col-4 text-end">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment