play video result done message working

parent e806a118
...@@ -1624,12 +1624,17 @@ class GamesThread(ThreadedComponent): ...@@ -1624,12 +1624,17 @@ class GamesThread(ThreadedComponent):
logger.info(f"Processing PLAY_VIDEO_MATCH_DONE for fixture {fixture_id}, match {match_id}") logger.info(f"Processing PLAY_VIDEO_MATCH_DONE for fixture {fixture_id}, match {match_id}")
# DEBUG: Log the full message data
logger.info(f"DEBUG PLAY_VIDEO_MATCH_DONE: message.data = {message.data}")
# Set match status to 'ingame' # Set match status to 'ingame'
self._set_match_status(match_id, 'ingame') self._set_match_status(match_id, 'ingame')
# Query database for the previously extracted result # Query database for the previously extracted result
extracted_result = self._query_extracted_result(match_id) extracted_result = self._query_extracted_result(match_id)
logger.info(f"DEBUG PLAY_VIDEO_MATCH_DONE: extracted_result = '{extracted_result}'")
if extracted_result: if extracted_result:
logger.info(f"Found extracted result for match {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) # Send PLAY_VIDEO_RESULTS message (note: RESULTS plural as per user request)
...@@ -1643,6 +1648,8 @@ class GamesThread(ThreadedComponent): ...@@ -1643,6 +1648,8 @@ class GamesThread(ThreadedComponent):
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}")
import traceback
logger.error(f"DEBUG PLAY_VIDEO_MATCH_DONE: Exception traceback: {traceback.format_exc()}")
def _handle_play_video_result_done(self, message: Message): def _handle_play_video_result_done(self, message: Message):
"""Handle PLAY_VIDEO_RESULTS_DONE message - result video finished, send MATCH_DONE and START_INTRO""" """Handle PLAY_VIDEO_RESULTS_DONE message - result video finished, send MATCH_DONE and START_INTRO"""
...@@ -1653,6 +1660,9 @@ class GamesThread(ThreadedComponent): ...@@ -1653,6 +1660,9 @@ class GamesThread(ThreadedComponent):
logger.info(f"Processing PLAY_VIDEO_RESULTS_DONE for fixture {fixture_id}, match {match_id}, result {result}") logger.info(f"Processing PLAY_VIDEO_RESULTS_DONE for fixture {fixture_id}, match {match_id}, result {result}")
# DEBUG: Log the full message data
logger.info(f"DEBUG PLAY_VIDEO_RESULTS_DONE: message.data = {message.data}")
# Update match status to 'done' and save result # Update match status to 'done' and save result
self._set_match_status_and_result(match_id, 'done', result) self._set_match_status_and_result(match_id, 'done', result)
...@@ -1674,9 +1684,34 @@ class GamesThread(ThreadedComponent): ...@@ -1674,9 +1684,34 @@ class GamesThread(ThreadedComponent):
logger.info(f"Processing MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}") logger.info(f"Processing MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}")
# DEBUG: Log the message data in detail
logger.info(f"DEBUG MATCH_DONE: message.data = {message.data}")
# DEBUG: Check current match state before update
session = self.db_manager.get_session()
try:
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
logger.info(f"DEBUG MATCH_DONE: Before update - match {match_id} status='{match.status}', result='{match.result}'")
else:
logger.error(f"DEBUG MATCH_DONE: Match {match_id} not found in database!")
finally:
session.close()
# Update match status to 'done' and save result # Update match status to 'done' and save result
self._set_match_status_and_result(match_id, 'done', result) self._set_match_status_and_result(match_id, 'done', result)
# DEBUG: Check match state after update
session = self.db_manager.get_session()
try:
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
logger.info(f"DEBUG MATCH_DONE: After update - match {match_id} status='{match.status}', result='{match.result}'")
else:
logger.error(f"DEBUG MATCH_DONE: Match {match_id} still not found after update!")
finally:
session.close()
# Wait 2 seconds then send NEXT_MATCH # Wait 2 seconds then send NEXT_MATCH
import time import time
time.sleep(2) time.sleep(2)
...@@ -1772,20 +1807,29 @@ class GamesThread(ThreadedComponent): ...@@ -1772,20 +1807,29 @@ class GamesThread(ThreadedComponent):
def _set_match_status_and_result(self, match_id: int, status: str, result: str): def _set_match_status_and_result(self, match_id: int, status: str, result: str):
"""Set match status and result in database""" """Set match status and result in database"""
try: try:
logger.info(f"DEBUG _set_match_status_and_result: Called with match_id={match_id}, status='{status}', result='{result}'")
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
match = session.query(MatchModel).filter_by(id=match_id).first() match = session.query(MatchModel).filter_by(id=match_id).first()
if match: if match:
logger.info(f"DEBUG _set_match_status_and_result: Found match {match_id}, current status='{match.status}', current result='{match.result}'")
match.status = status match.status = status
match.result = result match.result = result
session.commit() session.commit()
logger.info(f"Updated match {match_id} status to {status} and result to {result}") logger.info(f"Updated match {match_id} status to {status} and result to {result}")
# DEBUG: Verify the update
session.refresh(match)
logger.info(f"DEBUG _set_match_status_and_result: After commit - match.status='{match.status}', match.result='{match.result}'")
else: else:
logger.error(f"Match {match_id} not found") logger.error(f"Match {match_id} not found")
finally: finally:
session.close() session.close()
except Exception as e: except Exception as e:
logger.error(f"Failed to set match status and result: {e}") logger.error(f"Failed to set match status and result: {e}")
import traceback
logger.error(f"DEBUG _set_match_status_and_result: Exception traceback: {traceback.format_exc()}")
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"""
...@@ -1794,6 +1838,14 @@ class GamesThread(ThreadedComponent): ...@@ -1794,6 +1838,14 @@ class GamesThread(ThreadedComponent):
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
# DEBUG: Check if match exists and its current state
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
logger.info(f"🔍 [EXTRACTION DEBUG] Match {match_id} found - status='{match.status}', result='{match.result}', fixture_id='{match.fixture_id}'")
else:
logger.error(f"🔍 [EXTRACTION DEBUG] Match {match_id} NOT FOUND in database!")
return self._fallback_result_selection()
# Step 1: Get match outcomes to determine available result options # Step 1: Get match outcomes to determine available result options
logger.info(f"📊 [EXTRACTION DEBUG] Step 1: Retrieving match outcomes for match {match_id}") logger.info(f"📊 [EXTRACTION DEBUG] Step 1: Retrieving match outcomes for match {match_id}")
match_outcomes = session.query(MatchOutcomeModel).filter( match_outcomes = session.query(MatchOutcomeModel).filter(
...@@ -1803,6 +1855,10 @@ class GamesThread(ThreadedComponent): ...@@ -1803,6 +1855,10 @@ class GamesThread(ThreadedComponent):
available_outcome_names = [outcome.column_name for outcome in match_outcomes] 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}") logger.info(f"📊 [EXTRACTION DEBUG] Found {len(match_outcomes)} match outcomes: {available_outcome_names}")
# DEBUG: Log detailed outcome information
for outcome in match_outcomes:
logger.info(f"📊 [EXTRACTION DEBUG] Outcome: {outcome.column_name} = {outcome.float_value}")
# Step 2: Get result options that correspond to match outcomes (excluding UNDER/OVER) # 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") 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
...@@ -1909,6 +1965,12 @@ class GamesThread(ThreadedComponent): ...@@ -1909,6 +1965,12 @@ class GamesThread(ThreadedComponent):
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"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}") logger.info(f"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}")
# DEBUG: Final check - ensure result is not None
if selected_result is None:
logger.error(f"❌ [EXTRACTION DEBUG] CRITICAL: selected_result is None! This should not happen.")
return self._fallback_result_selection()
return selected_result return selected_result
finally: finally:
...@@ -1920,7 +1982,9 @@ class GamesThread(ThreadedComponent): ...@@ -1920,7 +1982,9 @@ class GamesThread(ThreadedComponent):
logger.error(f"❌ [EXTRACTION DEBUG] Full traceback: {traceback.format_exc()}") 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") logger.info(f"🔄 [EXTRACTION DEBUG] Using fallback random selection")
return self._fallback_result_selection() fallback_result = self._fallback_result_selection()
logger.info(f"🔄 [EXTRACTION DEBUG] Fallback result: {fallback_result}")
return fallback_result
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:
"""Perform weighted random selection based on inverse coefficients""" """Perform weighted random selection based on inverse coefficients"""
...@@ -1987,11 +2051,15 @@ class GamesThread(ThreadedComponent): ...@@ -1987,11 +2051,15 @@ class GamesThread(ThreadedComponent):
def _update_bet_results(self, match_id: int, selected_result: str, session): def _update_bet_results(self, match_id: int, selected_result: str, session):
"""Update bet results for UNDER/OVER and selected result with win amount calculation""" """Update bet results for UNDER/OVER and selected result with win amount calculation"""
try: try:
logger.info(f"DEBUG _update_bet_results: Starting for match {match_id}, selected_result='{selected_result}'")
# Get coefficient for the selected result # Get coefficient for the selected result
win_coefficient = self._get_outcome_coefficient(match_id, selected_result, session) win_coefficient = self._get_outcome_coefficient(match_id, selected_result, session)
logger.info(f"DEBUG _update_bet_results: win_coefficient = {win_coefficient}")
# Update UNDER/OVER bets - they win if they match the UNDER/OVER outcome # Update UNDER/OVER bets - they win if they match the UNDER/OVER outcome
under_over_outcome = 'UNDER' if selected_result == 'UNDER' else 'OVER' if selected_result == 'OVER' else None under_over_outcome = 'UNDER' if selected_result == 'UNDER' else 'OVER' if selected_result == 'OVER' else None
logger.info(f"DEBUG _update_bet_results: under_over_outcome = '{under_over_outcome}'")
if under_over_outcome: if under_over_outcome:
# UNDER/OVER bet wins # UNDER/OVER bet wins
...@@ -2001,24 +2069,29 @@ class GamesThread(ThreadedComponent): ...@@ -2001,24 +2069,29 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result == 'pending' BetDetailModel.result == 'pending'
).all() ).all()
logger.info(f"DEBUG _update_bet_results: Found {len(under_over_bets)} winning {under_over_outcome} bets")
for bet in under_over_bets: for bet in under_over_bets:
win_amount = bet.amount * win_coefficient win_amount = bet.amount * win_coefficient
bet.set_result('win', win_amount) bet.set_result('win', win_amount)
logger.info(f"DEBUG _update_bet_results: Set bet {bet.id} to win with amount {win_amount}")
# Other UNDER/OVER bet loses # Other UNDER/OVER bet loses
other_under_over = 'OVER' if under_over_outcome == 'UNDER' else 'UNDER' other_under_over = 'OVER' if under_over_outcome == 'UNDER' else 'UNDER'
session.query(BetDetailModel).filter( losing_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.outcome == other_under_over, BetDetailModel.outcome == other_under_over,
BetDetailModel.result == 'pending' BetDetailModel.result == 'pending'
).update({'result': 'lost'}) ).update({'result': 'lost'})
logger.info(f"DEBUG _update_bet_results: Set {losing_count} {other_under_over} bets to lost")
else: else:
# No UNDER/OVER result selected, all UNDER/OVER bets lose # No UNDER/OVER result selected, all UNDER/OVER bets lose
session.query(BetDetailModel).filter( losing_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.outcome.in_(['UNDER', 'OVER']), BetDetailModel.outcome.in_(['UNDER', 'OVER']),
BetDetailModel.result == 'pending' BetDetailModel.result == 'pending'
).update({'result': 'lost'}) ).update({'result': 'lost'})
logger.info(f"DEBUG _update_bet_results: Set {losing_count} UNDER/OVER bets to lost (no UNDER/OVER result)")
# Update bets for the selected result to 'win' (if not UNDER/OVER) # Update bets for the selected result to 'win' (if not UNDER/OVER)
if selected_result not in ['UNDER', 'OVER']: if selected_result not in ['UNDER', 'OVER']:
...@@ -2028,22 +2101,37 @@ class GamesThread(ThreadedComponent): ...@@ -2028,22 +2101,37 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result == 'pending' BetDetailModel.result == 'pending'
).all() ).all()
logger.info(f"DEBUG _update_bet_results: Found {len(winning_bets)} winning {selected_result} bets")
for bet in winning_bets: for bet in winning_bets:
win_amount = bet.amount * win_coefficient win_amount = bet.amount * win_coefficient
bet.set_result('win', win_amount) bet.set_result('win', win_amount)
logger.info(f"DEBUG _update_bet_results: Set bet {bet.id} to win with amount {win_amount}")
# Update all other bets to 'lost' # Update all other bets to 'lost'
session.query(BetDetailModel).filter( losing_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id, BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending', BetDetailModel.result == 'pending',
~BetDetailModel.outcome.in_([selected_result, 'UNDER', 'OVER']) ~BetDetailModel.outcome.in_([selected_result, 'UNDER', 'OVER'])
).update({'result': 'lost'}) ).update({'result': 'lost'})
logger.info(f"DEBUG _update_bet_results: Set {losing_count} other bets to lost")
# Update the match result in the matches table
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
logger.info(f"DEBUG _update_bet_results: Before update - match.result = '{match.result}'")
match.result = selected_result
logger.info(f"Updated match {match_id} result to {selected_result}")
else:
logger.error(f"DEBUG _update_bet_results: Match {match_id} not found for result update!")
session.commit() session.commit()
logger.info(f"Updated bet results for match {match_id}: winner={selected_result}, coefficient={win_coefficient}") logger.info(f"Updated bet results for match {match_id}: winner={selected_result}, coefficient={win_coefficient}")
except Exception as e: except Exception as e:
logger.error(f"Failed to update bet results: {e}") logger.error(f"Failed to update bet results: {e}")
import traceback
logger.error(f"DEBUG _update_bet_results: Exception traceback: {traceback.format_exc()}")
session.rollback() session.rollback()
def _collect_match_statistics(self, match_id: int, fixture_id: str, selected_result: str, session): def _collect_match_statistics(self, match_id: int, fixture_id: str, selected_result: str, session):
...@@ -2187,6 +2275,8 @@ class GamesThread(ThreadedComponent): ...@@ -2187,6 +2275,8 @@ class GamesThread(ThreadedComponent):
def _query_extracted_result(self, match_id: int) -> Optional[str]: def _query_extracted_result(self, match_id: int) -> Optional[str]:
"""Query database for previously extracted result""" """Query database for previously extracted result"""
try: try:
logger.info(f"DEBUG _query_extracted_result: Querying for match {match_id}")
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
# Query for the most recent extraction stats for this match # Query for the most recent extraction stats for this match
...@@ -2195,6 +2285,13 @@ class GamesThread(ThreadedComponent): ...@@ -2195,6 +2285,13 @@ class GamesThread(ThreadedComponent):
ExtractionStatsModel.match_id == match_id ExtractionStatsModel.match_id == match_id
).order_by(ExtractionStatsModel.created_at.desc()).first() ).order_by(ExtractionStatsModel.created_at.desc()).first()
logger.info(f"DEBUG _query_extracted_result: Query result - extraction_stats exists: {extraction_stats is not None}")
if extraction_stats:
logger.info(f"DEBUG _query_extracted_result: extraction_stats.actual_result = '{extraction_stats.actual_result}'")
logger.info(f"DEBUG _query_extracted_result: extraction_stats.extraction_result = '{extraction_stats.extraction_result}'")
logger.info(f"DEBUG _query_extracted_result: extraction_stats.created_at = {extraction_stats.created_at}")
if extraction_stats and extraction_stats.actual_result: if extraction_stats and extraction_stats.actual_result:
logger.info(f"Found extracted result for match {match_id}: {extraction_stats.actual_result}") logger.info(f"Found extracted result for match {match_id}: {extraction_stats.actual_result}")
return extraction_stats.actual_result return extraction_stats.actual_result
...@@ -2207,6 +2304,8 @@ class GamesThread(ThreadedComponent): ...@@ -2207,6 +2304,8 @@ class GamesThread(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to query extracted result for match {match_id}: {e}") logger.error(f"Failed to query extracted result for match {match_id}: {e}")
import traceback
logger.error(f"DEBUG _query_extracted_result: Exception traceback: {traceback.format_exc()}")
return None return None
def _determine_under_over_result(self, match_id: int, main_result: str) -> Optional[str]: def _determine_under_over_result(self, match_id: int, main_result: str) -> Optional[str]:
...@@ -2265,8 +2364,11 @@ class GamesThread(ThreadedComponent): ...@@ -2265,8 +2364,11 @@ class GamesThread(ThreadedComponent):
def _send_play_video_results(self, fixture_id: str, match_id: int, result: str): def _send_play_video_results(self, fixture_id: str, match_id: int, result: str):
"""Send PLAY_VIDEO_RESULTS message (plural as per user request)""" """Send PLAY_VIDEO_RESULTS message (plural as per user request)"""
try: try:
logger.info(f"DEBUG _send_play_video_results: Sending PLAY_VIDEO_RESULTS with fixture_id={fixture_id}, match_id={match_id}, result='{result}'")
# Determine under/over result separately from main result # Determine under/over result separately from main result
under_over_result = self._determine_under_over_result(match_id, result) under_over_result = self._determine_under_over_result(match_id, result)
logger.info(f"DEBUG _send_play_video_results: under_over_result = '{under_over_result}'")
play_results_message = MessageBuilder.play_video_result( play_results_message = MessageBuilder.play_video_result(
sender=self.name, sender=self.name,
...@@ -2275,11 +2377,17 @@ class GamesThread(ThreadedComponent): ...@@ -2275,11 +2377,17 @@ class GamesThread(ThreadedComponent):
result=result, result=result,
under_over_result=under_over_result under_over_result=under_over_result
) )
# DEBUG: Log the message content
logger.info(f"DEBUG _send_play_video_results: Message data: {play_results_message.data}")
self.message_bus.publish(play_results_message) 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}") 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: except Exception as e:
logger.error(f"Failed to send PLAY_VIDEO_RESULTS: {e}") logger.error(f"Failed to send PLAY_VIDEO_RESULTS: {e}")
import traceback
logger.error(f"DEBUG _send_play_video_results: Exception traceback: {traceback.format_exc()}")
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"""
...@@ -2299,17 +2407,25 @@ class GamesThread(ThreadedComponent): ...@@ -2299,17 +2407,25 @@ class GamesThread(ThreadedComponent):
def _send_match_done(self, fixture_id: str, match_id: int, result: str = None): def _send_match_done(self, fixture_id: str, match_id: int, result: str = None):
"""Send MATCH_DONE message""" """Send MATCH_DONE message"""
try: try:
logger.info(f"DEBUG _send_match_done: Sending MATCH_DONE with fixture_id={fixture_id}, match_id={match_id}, result='{result}'")
match_done_message = MessageBuilder.match_done( match_done_message = MessageBuilder.match_done(
sender=self.name, sender=self.name,
fixture_id=fixture_id, fixture_id=fixture_id,
match_id=match_id, match_id=match_id,
result=result result=result
) )
# DEBUG: Log the message content
logger.info(f"DEBUG _send_match_done: Message data: {match_done_message.data}")
self.message_bus.publish(match_done_message) self.message_bus.publish(match_done_message)
logger.info(f"Sent MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}") logger.info(f"Sent MATCH_DONE for fixture {fixture_id}, match {match_id}, result {result}")
except Exception as e: except Exception as e:
logger.error(f"Failed to send MATCH_DONE: {e}") logger.error(f"Failed to send MATCH_DONE: {e}")
import traceback
logger.error(f"DEBUG _send_match_done: Exception traceback: {traceback.format_exc()}")
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"""
...@@ -2353,6 +2469,9 @@ class GamesThread(ThreadedComponent): ...@@ -2353,6 +2469,9 @@ class GamesThread(ThreadedComponent):
try: try:
now = datetime.utcnow() now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# Find the maximum match_number in the fixture and increment from there # Find the maximum match_number in the fixture and increment from there
max_match_number = session.query(MatchModel.match_number).filter( max_match_number = session.query(MatchModel.match_number).filter(
MatchModel.fixture_id == fixture_id MatchModel.fixture_id == fixture_id
...@@ -2368,7 +2487,7 @@ class GamesThread(ThreadedComponent): ...@@ -2368,7 +2487,7 @@ class GamesThread(ThreadedComponent):
fighter2_township=old_match.fighter2_township, fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township, venue_kampala_township=old_match.venue_kampala_township,
start_time=now, start_time=now,
status='scheduled', status=new_match_status,
fixture_id=fixture_id, fixture_id=fixture_id,
filename=old_match.filename, filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum, file_sha1sum=old_match.file_sha1sum,
...@@ -2376,7 +2495,11 @@ class GamesThread(ThreadedComponent): ...@@ -2376,7 +2495,11 @@ class GamesThread(ThreadedComponent):
zip_filename=old_match.zip_filename, zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum, zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available zip_upload_status='completed', # Assume ZIP is already available
fixture_active_time=int(now.timestamp()) fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
) )
session.add(new_match) session.add(new_match)
...@@ -2391,11 +2514,11 @@ class GamesThread(ThreadedComponent): ...@@ -2391,11 +2514,11 @@ class GamesThread(ThreadedComponent):
) )
session.add(new_outcome) session.add(new_outcome)
logger.debug(f"Created new match #{match_number} from old match #{old_match.match_number}") logger.debug(f"Created new match #{match_number} from old match #{old_match.match_number} with status {new_match_status}")
match_number += 1 match_number += 1
session.commit() session.commit()
logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id}") logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id} with status {new_match_status}")
except Exception as e: except Exception as e:
logger.error(f"Failed to create matches from old matches: {e}") logger.error(f"Failed to create matches from old matches: {e}")
...@@ -2428,7 +2551,11 @@ class GamesThread(ThreadedComponent): ...@@ -2428,7 +2551,11 @@ class GamesThread(ThreadedComponent):
zip_filename=old_match.zip_filename, zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum, zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available zip_upload_status='completed', # Assume ZIP is already available
fixture_active_time=int(now.timestamp()) fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
) )
session.add(new_match) session.add(new_match)
...@@ -2607,7 +2734,7 @@ class GamesThread(ThreadedComponent): ...@@ -2607,7 +2734,7 @@ class GamesThread(ThreadedComponent):
return None return None
def _select_random_completed_matches_excluding_last(self, count: int, exclude_match_id: Optional[int], session) -> List[MatchModel]: 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""" """Select random completed matches from the database, excluding matches with same fighters as the last played match"""
try: try:
# Build query for completed matches # Build query for completed matches
query = session.query(MatchModel).filter( query = session.query(MatchModel).filter(
...@@ -2615,26 +2742,65 @@ class GamesThread(ThreadedComponent): ...@@ -2615,26 +2742,65 @@ class GamesThread(ThreadedComponent):
MatchModel.active_status == True MatchModel.active_status == True
) )
# Exclude the specified match if provided # Exclude matches with same fighters as the last played match
if exclude_match_id: if exclude_match_id:
query = query.filter(MatchModel.id != exclude_match_id) last_match = session.query(MatchModel).filter(MatchModel.id == exclude_match_id).first()
if last_match:
# Exclude matches with same fighter combinations (both directions)
query = query.filter(
~((MatchModel.fighter1_township == last_match.fighter1_township) &
(MatchModel.fighter2_township == last_match.fighter2_township)) &
~((MatchModel.fighter1_township == last_match.fighter2_township) &
(MatchModel.fighter2_township == last_match.fighter1_township))
)
logger.info(f"Excluding matches with fighters: {last_match.fighter1_township} vs {last_match.fighter2_township}")
completed_matches = query.all() completed_matches = query.all()
if len(completed_matches) < count: if len(completed_matches) < count:
logger.warning(f"Only {len(completed_matches)} completed matches available (excluding last played), requested {count}") logger.warning(f"Only {len(completed_matches)} completed matches available (excluding same fighters), requested {count}")
return completed_matches return completed_matches
# Select random matches # Select random matches
import random import random
selected_matches = random.sample(completed_matches, count) selected_matches = random.sample(completed_matches, count)
logger.info(f"Selected {len(selected_matches)} random completed matches (excluding match ID {exclude_match_id})") logger.info(f"Selected {len(selected_matches)} random completed matches (excluding same fighters as last match)")
return selected_matches return selected_matches
except Exception as e: except Exception as e:
logger.error(f"Failed to select random completed matches excluding {exclude_match_id}: {e}") logger.error(f"Failed to select random completed matches excluding same fighters: {e}")
return [] return []
def _determine_new_match_status(self, fixture_id: str, session) -> str:
"""Determine the status for new matches based on system state"""
try:
# Check if system is ingame (has any match with status 'ingame')
ingame_match = session.query(MatchModel).filter(
MatchModel.status == 'ingame',
MatchModel.active_status == True
).first()
if ingame_match:
# System is ingame, check if last 4 matches in the fixture are in 'bet' status
last_4_matches = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.active_status == True
).order_by(MatchModel.match_number.desc()).limit(4).all()
if len(last_4_matches) >= 4:
# Check if all last 4 matches are in 'bet' status
if all(match.status == 'bet' for match in last_4_matches):
logger.info(f"System is ingame and last 4 matches in fixture {fixture_id} are in bet status - new matches will be in bet status")
return 'bet'
# Default status
logger.info(f"New matches will be in scheduled status (system not ingame or last 4 matches not all in bet status)")
return 'scheduled'
except Exception as e:
logger.error(f"Failed to determine new match status: {e}")
return 'scheduled' # Default fallback
def _cleanup_previous_match_extractions(self): def _cleanup_previous_match_extractions(self):
"""Clean up all previous unzipped match directories from temporary location""" """Clean up all previous unzipped match directories from temporary location"""
try: try:
......
...@@ -5,7 +5,8 @@ Server-side match timer component for synchronized countdown across all clients ...@@ -5,7 +5,8 @@ Server-side match timer component for synchronized countdown across all clients
import time import time
import logging import logging
import threading import threading
from typing import Dict, Any, Optional import random
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta from datetime import datetime, timedelta
from .thread_manager import ThreadedComponent from .thread_manager import ThreadedComponent
...@@ -396,17 +397,30 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -396,17 +397,30 @@ class MatchTimerComponent(ThreadedComponent):
target_fixture_id = target_match.fixture_id target_fixture_id = target_match.fixture_id
if target_match: if target_match:
fixture_id = target_fixture_id or target_match.fixture_id
# Ensure there are at least 5 next matches in the fixture before sending START_INTRO
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id, session)
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) - ensuring minimum matches")
self._ensure_minimum_matches_in_fixture(fixture_id, 5 - remaining_matches, session)
# Recount after adding matches
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id, session)
logger.info(f"After ensuring minimum matches, fixture {fixture_id} now has {remaining_matches} remaining matches")
# Send START_INTRO message # Send START_INTRO message
start_intro_message = MessageBuilder.start_intro( start_intro_message = MessageBuilder.start_intro(
sender=self.name, sender=self.name,
fixture_id=target_fixture_id or target_match.fixture_id, fixture_id=fixture_id,
match_id=target_match.id match_id=target_match.id
) )
self.message_bus.publish(start_intro_message) self.message_bus.publish(start_intro_message)
return { return {
"fixture_id": target_fixture_id or target_match.fixture_id, "fixture_id": fixture_id,
"match_id": target_match.id, "match_id": target_match.id,
"match_number": target_match.match_number "match_number": target_match.match_number
} }
...@@ -462,6 +476,197 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -462,6 +476,197 @@ class MatchTimerComponent(ThreadedComponent):
logger.error(f"Failed to get match interval: {e}") logger.error(f"Failed to get match interval: {e}")
return 20 return 20
def _count_remaining_matches_in_fixture(self, fixture_id: str, session) -> int:
"""Count remaining matches in fixture that can still be played"""
try:
from ..database.models import MatchModel
# 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()
return remaining_count
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, session):
"""Ensure fixture has at least minimum_required matches by creating new ones from old completed matches"""
try:
from ..database.models import MatchModel
logger.info(f"Ensuring fixture {fixture_id} has at least {minimum_required} additional matches")
# 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}")
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:
from ..database.models import MatchModel
# 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:
return last_match.id
else:
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[Any]:
"""Select random completed matches from the database, excluding matches with same fighters as the last played match"""
try:
from ..database.models import MatchModel
# Build query for completed matches
query = session.query(MatchModel).filter(
MatchModel.status.in_(['done', 'cancelled', 'failed']),
MatchModel.active_status == True
)
# Exclude matches with same fighters as the last played match
if exclude_match_id:
last_match = session.query(MatchModel).filter(MatchModel.id == exclude_match_id).first()
if last_match:
# Exclude matches with same fighter combinations (both directions)
query = query.filter(
~((MatchModel.fighter1_township == last_match.fighter1_township) &
(MatchModel.fighter2_township == last_match.fighter2_township)) &
~((MatchModel.fighter1_township == last_match.fighter2_township) &
(MatchModel.fighter2_township == last_match.fighter1_township))
)
completed_matches = query.all()
if len(completed_matches) < count:
logger.warning(f"Only {len(completed_matches)} completed matches available (excluding same fighters), requested {count}")
return completed_matches
# Select random matches
selected_matches = random.sample(completed_matches, count)
logger.info(f"Selected {len(selected_matches)} random completed matches (excluding same fighters as last match)")
return selected_matches
except Exception as e:
logger.error(f"Failed to select random completed matches excluding same fighters: {e}")
return []
def _create_matches_from_old_matches(self, fixture_id: str, old_matches: List[Any], session):
"""Create new matches in the fixture by copying from old completed matches"""
try:
from ..database.models import MatchModel, MatchOutcomeModel
now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# Find the maximum match_number in the fixture and increment from there
max_match_number = session.query(MatchModel.match_number).filter(
MatchModel.fixture_id == fixture_id
).order_by(MatchModel.match_number.desc()).first()
match_number = (max_match_number[0] + 1) if max_match_number else 1
for old_match in old_matches:
# Create a new match based on the old one
new_match = MatchModel(
match_number=match_number,
fighter1_township=old_match.fighter1_township,
fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township,
start_time=now,
status=new_match_status,
fixture_id=fixture_id,
filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum,
active_status=True,
zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
fixture_active_time=int(now.timestamp())
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes
for outcome in old_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=outcome.column_name,
float_value=outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created new match #{match_number} from old match #{old_match.match_number} with status {new_match_status}")
match_number += 1
session.commit()
logger.info(f"Created {len(old_matches)} new matches in fixture {fixture_id} with status {new_match_status}")
except Exception as e:
logger.error(f"Failed to create matches from old matches: {e}")
session.rollback()
raise
def _determine_new_match_status(self, fixture_id: str, session) -> str:
"""Determine the status for new matches based on system state"""
try:
from ..database.models import MatchModel
# Check if system is ingame (has any match with status 'ingame')
ingame_match = session.query(MatchModel).filter(
MatchModel.status == 'ingame',
MatchModel.active_status == True
).first()
if ingame_match:
# System is ingame, check if last 4 matches in the fixture are in 'bet' status
last_4_matches = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.active_status == True
).order_by(MatchModel.match_number.desc()).limit(4).all()
if len(last_4_matches) >= 4:
# Check if all last 4 matches are in 'bet' status
if all(match.status == 'bet' for match in last_4_matches):
logger.info(f"System is ingame and last 4 matches in fixture {fixture_id} are in bet status - new matches will be in bet status")
return 'bet'
# Default status
return 'scheduled'
except Exception as e:
logger.error(f"Failed to determine new match status: {e}")
return 'scheduled' # Default fallback
def _send_timer_update(self): def _send_timer_update(self):
"""Send timer update message to all clients""" """Send timer update message to all clients"""
try: try:
......
...@@ -1749,6 +1749,7 @@ class PlayerWindow(QMainWindow): ...@@ -1749,6 +1749,7 @@ class PlayerWindow(QMainWindow):
self.loop_count = 0 self.loop_count = 0
self.current_loop_iteration = 0 self.current_loop_iteration = 0
self.current_file_path = None self.current_file_path = None
logger.info("RESULT DEBUG: Initialized current_file_path = None")
# Match video tracking # Match video tracking
self.is_playing_match_video = False self.is_playing_match_video = False
...@@ -1933,6 +1934,7 @@ class PlayerWindow(QMainWindow): ...@@ -1933,6 +1934,7 @@ class PlayerWindow(QMainWindow):
logger.info(f"QUrl path: {url.path()}") logger.info(f"QUrl path: {url.path()}")
# Store current file path for loop functionality # Store current file path for loop functionality
logger.info(f"RESULT DEBUG: Setting current_file_path = '{str(absolute_path)}'")
self.current_file_path = str(absolute_path) self.current_file_path = str(absolute_path)
logger.info(f"Media player current state: {self.media_player.playbackState()}") logger.info(f"Media player current state: {self.media_player.playbackState()}")
...@@ -2185,17 +2187,70 @@ class PlayerWindow(QMainWindow): ...@@ -2185,17 +2187,70 @@ class PlayerWindow(QMainWindow):
# Check if this is the end of a result video (any result video) # 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 "" 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: # Dynamically get result video filenames from database (excludes OVER/UNDER)
logger.info(f"RESULT DEBUG: Result video {current_filename} ended - waiting 5 seconds then sending PLAY_VIDEO_RESULTS_DONE") result_videos = self.qt_player._get_result_video_filenames() if hasattr(self.qt_player, '_get_result_video_filenames') else ["WIN1.mp4", "WIN2.mp4", "KO1.mp4", "KO2.mp4", "DRAW.mp4", "RET1.mp4", "RET2.mp4"]
# Wait 5 seconds to allow results overlay template to show results logger.info(f"RESULT DEBUG: Dynamically loaded result_videos = {result_videos}")
import time
time.sleep(5) # DEBUG: Add comprehensive logging for result video end detection
if self.qt_player: logger.info(f"RESULT DEBUG: Checking for result video end detection")
self.qt_player._send_result_video_done_message(result_info=self.current_result_video_info) logger.info(f"RESULT DEBUG: current_file_path = {self.current_file_path}")
# Reset result video tracking logger.info(f"RESULT DEBUG: current_filename = {current_filename}")
self.current_result_video_info = None
# Check if current filename is a result video
is_result_video = current_filename in result_videos
logger.info(f"RESULT DEBUG: current_filename in result_videos = {is_result_video}")
if is_result_video:
logger.info(f"RESULT DEBUG: Result video {current_filename} detected - extracting information and scheduling PLAY_VIDEO_RESULTS_DONE in 5 seconds")
# Extract result from filename
result = current_filename.replace('.mp4', '')
# Extract match_id from path like /tmp/match_29_.../
import re
match = re.search(r'/match_(\d+)_', self.current_file_path)
match_id = int(match.group(1)) if match else None
# Try to get fixture_id from database using match_id
fixture_id = None
if match_id:
try:
from ..database.models import MatchModel
db_manager = self.qt_player._get_database_manager() if hasattr(self, 'qt_player') else None
if db_manager:
session = db_manager.get_session()
try:
match_obj = session.query(MatchModel).filter_by(id=match_id).first()
if match_obj:
fixture_id = match_obj.fixture_id
finally:
session.close()
except Exception as e:
logger.debug(f"Failed to get fixture_id from database: {e}")
# Create result_video_info
result_video_info = {
'path': self.current_file_path,
'fixture_id': fixture_id,
'match_id': match_id,
'result': result,
'duration': 10.0, # Default duration
'is_result_video': True
}
# Set tracking attributes for the done message
self.current_result_video_info = result_video_info
self.current_result_video_filename = current_filename
logger.info(f"RESULT DEBUG: Extracted result video info: {result_video_info}")
# Schedule sending PLAY_VIDEO_RESULTS_DONE message after 5 seconds (non-blocking)
# This allows the results overlay template to show results before sending the done message
QTimer.singleShot(5000, self.qt_player._send_result_video_done_after_delay)
return return
else:
logger.info(f"RESULT DEBUG: Not a result video - continuing with normal flow")
# Handle loop functionality for intro videos # Handle loop functionality for intro videos
if self.loop_enabled: if self.loop_enabled:
...@@ -3128,21 +3183,6 @@ class QtVideoPlayer(QObject): ...@@ -3128,21 +3183,6 @@ class QtVideoPlayer(QObject):
) )
self.message_bus.publish(progress_message, broadcast=True) self.message_bus.publish(progress_message, broadcast=True)
# Check if we're playing a result video and are 3 seconds from the end
if (hasattr(self, 'current_result_video_info') and
self.current_result_video_info and
duration > 0):
position_seconds = position / 1000.0
duration_seconds = duration / 1000.0
time_remaining = duration_seconds - position_seconds
# Send MATCH_DONE when 3 seconds from end
if time_remaining <= 3.0 and not hasattr(self, '_match_done_sent'):
logger.info(f"Result video ending in {time_remaining:.1f} seconds, sending MATCH_DONE")
self._send_match_done_message()
self._match_done_sent = True # Prevent multiple sends
except Exception as e: except Exception as e:
logger.error(f"Failed to send progress update: {e}") logger.error(f"Failed to send progress update: {e}")
...@@ -4072,8 +4112,13 @@ class QtVideoPlayer(QObject): ...@@ -4072,8 +4112,13 @@ class QtVideoPlayer(QObject):
) )
# Store result video info for end-of-video handling # Store result video info for end-of-video handling
logger.info(f"RESULT DEBUG: Setting current_result_video_info = {result_video_info}")
self.current_result_video_info = result_video_info self.current_result_video_info = result_video_info
result_filename = Path(video_path).name
logger.info(f"RESULT DEBUG: Setting current_result_video_filename = '{result_filename}'")
self.current_result_video_filename = result_filename
logger.info(f"RESULT DEBUG: Result video tracking set - info: {self.current_result_video_info}, filename: '{self.current_result_video_filename}'")
logger.info(f"Result video started with results overlay template for result: {result}") logger.info(f"Result video started with results overlay template for result: {result}")
except Exception as e: except Exception as e:
...@@ -4150,56 +4195,130 @@ class QtVideoPlayer(QObject): ...@@ -4150,56 +4195,130 @@ 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): def _send_result_video_done_after_delay(self):
"""Send PLAY_VIDEO_RESULT_DONE message when result video finishes""" """Send PLAY_VIDEO_RESULT_DONE message after the 5-second delay"""
try: try:
# Use provided result_info or fall back to instance attribute logger.info("RESULT DEBUG: _send_result_video_done_after_delay called - 5 second delay has elapsed")
current_result_info = result_info if result_info is not None else getattr(self, 'current_result_video_info', None)
# Get the current result video info
current_result_info = getattr(self, 'current_result_video_info', None)
logger.info(f"RESULT DEBUG: current_result_info = {current_result_info}")
if current_result_info: if current_result_info:
logger.info("RESULT DEBUG: current_result_info is not None, proceeding to send message")
from ..core.message_bus import MessageBuilder from ..core.message_bus import MessageBuilder
fixture_id = current_result_info.get('fixture_id')
match_id = current_result_info.get('match_id')
result = current_result_info.get('result')
logger.info(f"RESULT DEBUG: Extracted values - fixture_id={fixture_id}, match_id={match_id}, result={result}")
done_message = MessageBuilder.play_video_result_done( done_message = MessageBuilder.play_video_result_done(
sender=self.name, sender=self.name,
fixture_id=current_result_info.get('fixture_id'), fixture_id=fixture_id,
match_id=current_result_info.get('match_id'), match_id=match_id,
result=current_result_info.get('result') result=result
) )
self.message_bus.publish(done_message, broadcast=True) logger.info(f"RESULT DEBUG: Created PLAY_VIDEO_RESULT_DONE message: {done_message}")
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')}") logger.info(f"RESULT DEBUG: Message data: {done_message.data}")
publish_result = self.message_bus.publish(done_message, broadcast=True)
logger.info(f"RESULT DEBUG: Message publish result: {publish_result}")
logger.info(f"Sent PLAY_VIDEO_RESULT_DONE (after 5s delay) for fixture {fixture_id}, match {match_id}, result {result}")
# Reset result video tracking after sending the message
self.current_result_video_info = None
self.current_result_video_filename = None
logger.debug("RESULT DEBUG: Reset result video tracking after sending PLAY_VIDEO_RESULT_DONE")
else:
logger.warning("RESULT DEBUG: current_result_info is None - cannot send PLAY_VIDEO_RESULT_DONE message")
except Exception as e: except Exception as e:
logger.error(f"Failed to send result video done message: {e}") logger.error(f"RESULT DEBUG: Failed to send result video done message after delay: {e}")
import traceback
logger.error(f"RESULT DEBUG: Full traceback: {traceback.format_exc()}")
def _send_match_done_message(self): def _get_result_video_filenames(self) -> List[str]:
"""Send MATCH_DONE message when result video is 3 seconds from ending""" """Dynamically get result video filenames from database, excluding OVER/UNDER"""
try: try:
if (hasattr(self, 'current_result_video_info') and # Get database manager from the QtVideoPlayer component
self.current_result_video_info): db_manager = None
if hasattr(self, 'qt_player') and self.qt_player:
# Try to get db_manager from qt_player's message bus
if hasattr(self.qt_player, 'message_bus') and self.qt_player.message_bus:
# Try to get db_manager from web_dashboard component
try:
web_dashboard_queue = self.qt_player.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'):
db_manager = component.db_manager
logger.debug("PlayerWindow: Got db_manager from web_dashboard component")
except Exception as e:
logger.debug(f"PlayerWindow: Could not get db_manager from message bus: {e}")
if not db_manager:
# 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"
db_manager = DatabaseManager(str(db_path))
if not db_manager.initialize():
logger.warning("PlayerWindow: Failed to initialize database manager")
# Return default result videos if database is not available (excluding OVER/UNDER)
return ["WIN1.mp4", "WIN2.mp4", "KO1.mp4", "KO2.mp4", "DRAW.mp4", "RET1.mp4", "RET2.mp4"]
session = db_manager.get_session()
try:
# Query active result options from database, excluding OVER and UNDER
from ..database.models import ResultOptionModel
active_results = session.query(ResultOptionModel).filter(
ResultOptionModel.is_active == True,
ResultOptionModel.result_name.notin_(['OVER', 'UNDER'])
).all()
# Convert result names to video filenames
result_videos = [f"{result.result_name}.mp4" for result in active_results]
logger.debug(f"PlayerWindow: Found {len(active_results)} active result options (excluding OVER/UNDER): {[r.result_name for r in active_results]}")
logger.debug(f"PlayerWindow: Generated result video filenames: {result_videos}")
return result_videos
finally:
session.close()
result_info = self.current_result_video_info except Exception as e:
fixture_id = result_info['fixture_id'] logger.error(f"PlayerWindow: Failed to get result video filenames from database: {e}")
match_id = result_info['match_id'] # Return default result videos as fallback (excluding OVER/UNDER)
return ["WIN1.mp4", "WIN2.mp4", "KO1.mp4", "KO2.mp4", "DRAW.mp4", "RET1.mp4", "RET2.mp4"]
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 from ..core.message_bus import MessageBuilder
done_message = MessageBuilder.match_done( done_message = MessageBuilder.play_video_result_done(
sender=self.name, sender=self.name,
fixture_id=fixture_id, fixture_id=current_result_info.get('fixture_id'),
match_id=match_id match_id=current_result_info.get('match_id'),
result=current_result_info.get('result')
) )
self.message_bus.publish(done_message, broadcast=True) self.message_bus.publish(done_message, broadcast=True)
logger.info(f"Sent MATCH_DONE for fixture {fixture_id}, match {match_id}") 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')}")
# Clear result video info
self.current_result_video_info = None
if hasattr(self, '_match_done_sent'):
delattr(self, '_match_done_sent')
except Exception as e: except Exception as e:
logger.error(f"Failed to send match done message: {e}") logger.error(f"Failed to send result video done message: {e}")
def _do_status_request(self, message: Message): def _do_status_request(self, message: Message):
"""Execute status request on main thread""" """Execute status request on main thread"""
......
...@@ -53,10 +53,21 @@ ...@@ -53,10 +53,21 @@
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: 1; /* Always visible */
animation: fadeInScale 1s ease-out forwards;
padding-bottom: 50px; padding-bottom: 50px;
display: none; /* Initially hidden until data is available */ display: flex; /* Always visible, content hidden instead */
}
.results-content {
width: 100%;
opacity: 0;
transform: translateY(20px);
transition: all 0.5s ease-out;
}
.results-content.visible {
opacity: 1;
transform: translateY(0);
} }
.results-title { .results-title {
...@@ -632,35 +643,111 @@ ...@@ -632,35 +643,111 @@
padding: 20px; padding: 20px;
grid-column: 1 / -1; grid-column: 1 / -1;
} }
/* Combined Result Display */
.combined-result-display {
text-align: center;
margin-bottom: 30px;
}
.combined-result-text {
font-size: 60px;
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: 400px;
}
/* Winning Bets Section */
.winning-bets-section {
width: 100%;
margin-top: 20px;
}
.bets-title {
color: white;
font-size: 32px;
font-weight: bold;
text-align: center;
margin-bottom: 20px;
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.5);
}
.bets-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px;
max-height: 300px;
overflow-y: auto;
padding: 10px;
}
.bet-item {
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;
min-width: 150px;
}
.bet-item: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;
}
</style> </style>
</head> </head>
<body> <body>
<div class="overlay-container"> <div class="overlay-container">
<div class="results-panel" id="resultsPanel" style="display: flex;"> <div class="results-panel" id="resultsPanel">
<!-- Title --> <div class="results-content" id="resultsContent">
<div class="results-title">RESULTS</div> <!-- Title -->
<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>
<!-- Main Result --> <!-- Fighters Display -->
<div class="main-result-display" id="mainResultDisplay"> <div class="fighters-display" id="fightersDisplay">
<div class="main-result-text" id="mainResultText">WIN1</div> <div class="fighter-names" id="fighterNames">
</div> <span id="fighter1">Fighter 1</span> VS <span id="fighter2">Fighter 2</span>
</div>
</div>
<!-- Under/Over Result --> <!-- Combined Result Display -->
<div class="under-over-display" id="underOverDisplay"> <div class="combined-result-display" id="combinedResultDisplay">
<div class="under-over-text" id="underOverText">UNDER</div> <div class="combined-result-text" id="combinedResultText">
</div> <span id="mainResult">WIN1</span> / <span id="underOverResult">UNDER</span>
</div>
</div>
<!-- Winning Outcomes Grid --> <!-- Winning Bets Section -->
<div class="winning-outcomes-section"> <div class="winning-bets-section">
<div class="outcomes-grid" id="outcomesGrid"> <div class="bets-title">Winning bets:</div>
<!-- Winning outcomes will be populated here --> <div class="bets-list" id="betsList">
<!-- Winning bets will be populated here -->
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -674,6 +761,11 @@ ...@@ -674,6 +761,11 @@
let currentMatch = null; let currentMatch = null;
let winningOutcomes = []; let winningOutcomes = [];
let animationStarted = false; let animationStarted = false;
let videoStarted = false;
let contentVisible = false;
let videoStartTime = null;
let contentDelayTimer = null;
let resultsTimer = null;
// Outcome categories for styling // Outcome categories for styling
const outcomeCategories = { const outcomeCategories = {
...@@ -728,33 +820,34 @@ ...@@ -728,33 +820,34 @@
fetchWinningOutcomes(data.match_id); fetchWinningOutcomes(data.match_id);
} }
// Start the animation sequence // Prepare data but don't start animation yet - wait for video to actually start playing
startResultsAnimation(); console.log('Results data received, preparing animation data');
prepareResultsAnimation();
} else { } else {
// No valid data, show loading state // No valid data, show loading state
showLoadingState(); showLoadingState();
} }
} }
// Start the results animation sequence // Prepare the results animation sequence (called when data is received)
function startResultsAnimation() { function prepareResultsAnimation() {
if (animationStarted) return; if (animationStarted) return;
animationStarted = true; animationStarted = true;
// Show results panel immediately // Show results panel immediately (but content hidden)
showResultsPanel(); showResultsPanel();
// Update fighters display // Update fighters display
updateFightersDisplay(); updateFightersDisplay();
// Update main result display // Update combined result display
updateMainResultDisplay(); updateCombinedResultDisplay();
// Update under/over result display // Update winning bets display
updateUnderOverResultDisplay(); updateWinningBetsDisplay();
// Update winning outcomes display // Content will be shown after 5 seconds when video starts
updateWinningOutcomesDisplay();
} }
// Fetch winning outcomes for the match // Fetch winning outcomes for the match
...@@ -768,7 +861,7 @@ ...@@ -768,7 +861,7 @@
const outcomesData = JSON.parse(outcomesJson); const outcomesData = JSON.parse(outcomesJson);
console.log('Received winning outcomes:', outcomesData); console.log('Received winning outcomes:', outcomesData);
winningOutcomes = outcomesData || []; winningOutcomes = outcomesData || [];
updateWinningOutcomesDisplay(); updateWinningBetsDisplay();
} catch (error) { } catch (error) {
console.error('Failed to get winning outcomes:', error); console.error('Failed to get winning outcomes:', error);
// Fallback: show sample data for testing // Fallback: show sample data for testing
...@@ -777,7 +870,7 @@ ...@@ -777,7 +870,7 @@
{ outcome: 'OVER', amount: 87.50 }, { outcome: 'OVER', amount: 87.50 },
{ outcome: 'KO1', amount: 95.00 } { outcome: 'KO1', amount: 95.00 }
]; ];
updateWinningOutcomesDisplay(); updateWinningBetsDisplay();
} }
} else { } else {
console.warn('Qt WebChannel not available for fetching winning outcomes'); console.warn('Qt WebChannel not available for fetching winning outcomes');
...@@ -787,7 +880,7 @@ ...@@ -787,7 +880,7 @@
{ outcome: 'OVER', amount: 87.50 }, { outcome: 'OVER', amount: 87.50 },
{ outcome: 'KO1', amount: 95.00 } { outcome: 'KO1', amount: 95.00 }
]; ];
updateWinningOutcomesDisplay(); updateWinningBetsDisplay();
} }
} }
...@@ -796,6 +889,38 @@ ...@@ -796,6 +889,38 @@
const resultsPanel = document.getElementById('resultsPanel'); const resultsPanel = document.getElementById('resultsPanel');
resultsPanel.style.display = 'flex'; resultsPanel.style.display = 'flex';
} }
// Show results content with animation after delay
function showResultsContent() {
const resultsContent = document.getElementById('resultsContent');
resultsContent.classList.add('visible');
}
// Handle video position changes to detect when video starts playing and reaches 5 seconds
function handlePositionChange(position, duration) {
// Check if video has started playing (position > 0)
if (position > 0 && !videoStarted) {
videoStarted = true;
console.log('Video started playing at position:', position);
}
// Check if video has been playing for at least 5 seconds
if (videoStarted && position >= 5 && !contentVisible) {
contentVisible = true;
console.log('Video has been playing for 5+ seconds, showing results content');
// Clear any existing timers
if (resultsTimer) {
clearTimeout(resultsTimer);
}
if (contentDelayTimer) {
clearTimeout(contentDelayTimer);
}
// Show results content with animation
showResultsContent();
}
}
// Update fighters display // Update fighters display
function updateFightersDisplay() { function updateFightersDisplay() {
...@@ -813,65 +938,63 @@ ...@@ -813,65 +938,63 @@
} }
} }
// Update main result display // Update combined result display
function updateMainResultDisplay() { function updateCombinedResultDisplay() {
const mainResultText = document.getElementById('mainResultText'); const mainResultSpan = document.getElementById('mainResult');
const mainResultDisplay = document.getElementById('mainResultDisplay'); const underOverSpan = document.getElementById('underOverResult');
const combinedDisplay = document.getElementById('combinedResultDisplay');
if (currentMainResult) { if (currentMainResult || currentUnderOverResult) {
mainResultText.textContent = currentMainResult; if (currentMainResult) {
mainResultDisplay.style.display = 'block'; mainResultSpan.textContent = currentMainResult;
} else { } else {
mainResultDisplay.style.display = 'none'; mainResultSpan.textContent = '';
} }
}
// Update under/over result display if (currentUnderOverResult) {
function updateUnderOverResultDisplay() { underOverSpan.textContent = currentUnderOverResult;
const underOverText = document.getElementById('underOverText'); } else {
const underOverDisplay = document.getElementById('underOverDisplay'); underOverSpan.textContent = '';
}
if (currentUnderOverResult) { combinedDisplay.style.display = 'block';
underOverText.textContent = currentUnderOverResult;
underOverDisplay.className = `under-over-display ${currentUnderOverResult.toLowerCase()}`;
underOverDisplay.style.display = 'block';
} else { } else {
underOverDisplay.style.display = 'none'; combinedDisplay.style.display = 'none';
} }
} }
// Update winning outcomes display with staggered animation // Update winning bets display with staggered animation
function updateWinningOutcomesDisplay() { function updateWinningBetsDisplay() {
const outcomesGrid = document.getElementById('outcomesGrid'); const betsList = document.getElementById('betsList');
outcomesGrid.innerHTML = ''; betsList.innerHTML = '';
if (!winningOutcomes || winningOutcomes.length === 0) { if (!winningOutcomes || winningOutcomes.length === 0) {
const noOutcomesDiv = document.createElement('div'); const noBetsDiv = document.createElement('div');
noOutcomesDiv.className = 'no-outcomes-message'; noBetsDiv.className = 'no-bets-message';
noOutcomesDiv.textContent = 'No winning outcomes for this match'; noBetsDiv.textContent = 'No winning bets for this match';
outcomesGrid.appendChild(noOutcomesDiv); betsList.appendChild(noBetsDiv);
return; return;
} }
// Create outcome cards for each winning outcome with staggered animation // Create bet items for each winning outcome with staggered animation
winningOutcomes.forEach((outcome, index) => { winningOutcomes.forEach((outcome, index) => {
const outcomeCard = document.createElement('div'); const betItem = document.createElement('div');
outcomeCard.className = 'outcome-card'; betItem.className = 'bet-item';
outcomeCard.style.opacity = '0'; betItem.style.opacity = '0';
outcomeCard.style.transform = 'translateY(20px)'; betItem.style.transform = 'translateY(20px)';
outcomeCard.innerHTML = ` betItem.innerHTML = `
<div class="outcome-name">${outcome.outcome || 'Unknown'}</div> <div class="bet-outcome">${outcome.outcome || 'Unknown'}</div>
<div class="outcome-amount">$${outcome.amount ? outcome.amount.toFixed(2) : '0.00'}</div> <div class="bet-amount">$${outcome.amount ? outcome.amount.toFixed(2) : '0.00'}</div>
`; `;
outcomesGrid.appendChild(outcomeCard); betsList.appendChild(betItem);
// Animate in with delay (staggered by 200ms) // Animate in with delay (staggered by 200ms)
setTimeout(() => { setTimeout(() => {
outcomeCard.style.transition = 'all 0.5s ease-out'; betItem.style.transition = 'all 0.5s ease-out';
outcomeCard.style.opacity = '1'; betItem.style.opacity = '1';
outcomeCard.style.transform = 'translateY(0)'; betItem.style.transform = 'translateY(0)';
}, index * 200); }, index * 200);
}); });
} }
...@@ -918,25 +1041,42 @@ ...@@ -918,25 +1041,42 @@
// Initialize when DOM is loaded // Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('Results overlay initialized'); console.log('Results overlay initialized');
// Always show results panel with default content // Always show results panel with default content
showResultsPanel(); showResultsPanel();
// Timer will start when video begins playing (detected via position changes)
console.log('Waiting for video to start playing before showing results content');
}); });
// Qt WebChannel initialization (when available) // Qt WebChannel initialization (when available)
if (typeof QWebChannel !== 'undefined') { if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) { new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('WebChannel initialized for results overlay'); console.log('WebChannel initialized for results overlay');
// Connect to overlay object if available // Connect to overlay object if available
if (channel.objects.overlay) { if (channel.objects.overlay) {
channel.objects.overlay.dataChanged.connect(function(data) { window.overlay = channel.objects.overlay;
// Connect dataChanged signal
window.overlay.dataChanged.connect(function(data) {
updateOverlayData(data); updateOverlayData(data);
}); });
// Connect positionChanged signal
if (window.overlay.positionChanged) {
window.overlay.positionChanged.connect(function(position, duration) {
if (position !== null && duration !== null) {
handlePositionChange(position, duration);
} else {
console.warn('positionChanged signal received null/undefined parameters, skipping');
}
});
}
// Get initial data // Get initial data
if (channel.objects.overlay.getCurrentData) { if (window.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) { window.overlay.getCurrentData(function(data) {
updateOverlayData(data); updateOverlayData(data);
}); });
} }
......
...@@ -32,13 +32,13 @@ class DashboardAPI: ...@@ -32,13 +32,13 @@ class DashboardAPI:
try: try:
# Get configuration status # Get configuration status
config_status = self.config_manager.validate_configuration() config_status = self.config_manager.validate_configuration()
# Get database status # Get database status
db_status = self.db_manager.get_connection_status() db_status = self.db_manager.get_connection_status()
# Get component status (cached or from message bus) # Get component status (cached or from message bus)
components_status = self._get_components_status() components_status = self._get_components_status()
return { return {
"status": "online", "status": "online",
"timestamp": datetime.utcnow().isoformat(), "timestamp": datetime.utcnow().isoformat(),
...@@ -47,7 +47,7 @@ class DashboardAPI: ...@@ -47,7 +47,7 @@ class DashboardAPI:
"database": db_status, "database": db_status,
"components": components_status "components": components_status
} }
except Exception as e: except Exception as e:
logger.error(f"Failed to get system status: {e}") logger.error(f"Failed to get system status: {e}")
return { return {
...@@ -55,6 +55,70 @@ class DashboardAPI: ...@@ -55,6 +55,70 @@ class DashboardAPI:
"error": str(e), "error": str(e),
"timestamp": datetime.utcnow().isoformat() "timestamp": datetime.utcnow().isoformat()
} }
def get_debug_match_status(self, fixture_id: str = None) -> Dict[str, Any]:
"""Get debug information about match statuses for troubleshooting"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel, MatchOutcomeModel, ExtractionStatsModel
# Get all matches or filter by fixture_id
query = session.query(MatchModel)
if fixture_id:
query = query.filter(MatchModel.fixture_id == fixture_id)
matches = query.order_by(MatchModel.match_number.desc()).limit(10).all()
debug_data = []
for match in matches:
match_data = {
"id": match.id,
"match_number": match.match_number,
"fixture_id": match.fixture_id,
"fighter1": match.fighter1_township,
"fighter2": match.fighter2_township,
"status": match.status,
"result": match.result,
"start_time": match.start_time.isoformat() if match.start_time else None,
"end_time": match.end_time.isoformat() if match.end_time else None,
"active_status": match.active_status
}
# Get outcomes
outcomes = session.query(MatchOutcomeModel).filter_by(match_id=match.id).all()
match_data["outcomes"] = [{"name": o.column_name, "value": o.float_value} for o in outcomes]
# Get extraction stats
extraction_stats = session.query(ExtractionStatsModel).filter_by(match_id=match.id).first()
if extraction_stats:
match_data["extraction_stats"] = {
"actual_result": extraction_stats.actual_result,
"extraction_result": extraction_stats.extraction_result,
"created_at": extraction_stats.created_at.isoformat()
}
else:
match_data["extraction_stats"] = None
debug_data.append(match_data)
return {
"success": True,
"debug_data": debug_data,
"fixture_id": fixture_id,
"timestamp": datetime.utcnow().isoformat()
}
finally:
session.close()
except Exception as e:
logger.error(f"Failed to get debug match status: {e}")
return {
"success": False,
"error": str(e),
"timestamp": datetime.utcnow().isoformat()
}
def get_video_status(self) -> Dict[str, Any]: def get_video_status(self) -> Dict[str, Any]:
"""Get video player status""" """Get video player status"""
......
...@@ -805,6 +805,18 @@ def system_status(): ...@@ -805,6 +805,18 @@ def system_status():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api_bp.route('/debug/match-status')
def debug_match_status():
"""Get debug information about match statuses"""
try:
fixture_id = request.args.get('fixture_id')
debug_data = api_bp.api.get_debug_match_status(fixture_id)
return jsonify(debug_data)
except Exception as e:
logger.error(f"API debug match status error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/video/status') @api_bp.route('/video/status')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def video_status(): def video_status():
......
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