play video result done message working

parent e806a118
......@@ -1624,12 +1624,17 @@ class GamesThread(ThreadedComponent):
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'
self._set_match_status(match_id, 'ingame')
# Query database for the previously extracted result
extracted_result = self._query_extracted_result(match_id)
logger.info(f"DEBUG PLAY_VIDEO_MATCH_DONE: extracted_result = '{extracted_result}'")
if 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)
......@@ -1643,6 +1648,8 @@ class GamesThread(ThreadedComponent):
except Exception as 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):
"""Handle PLAY_VIDEO_RESULTS_DONE message - result video finished, send MATCH_DONE and START_INTRO"""
......@@ -1653,6 +1660,9 @@ class GamesThread(ThreadedComponent):
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
self._set_match_status_and_result(match_id, 'done', result)
......@@ -1674,9 +1684,34 @@ class GamesThread(ThreadedComponent):
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
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
import time
time.sleep(2)
......@@ -1772,20 +1807,29 @@ class GamesThread(ThreadedComponent):
def _set_match_status_and_result(self, match_id: int, status: str, result: str):
"""Set match status and result in database"""
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()
try:
match = session.query(MatchModel).filter_by(id=match_id).first()
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.result = result
session.commit()
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:
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}")
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:
"""Perform complex result extraction logic"""
......@@ -1794,6 +1838,14 @@ class GamesThread(ThreadedComponent):
session = self.db_manager.get_session()
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
logger.info(f"📊 [EXTRACTION DEBUG] Step 1: Retrieving match outcomes for match {match_id}")
match_outcomes = session.query(MatchOutcomeModel).filter(
......@@ -1803,6 +1855,10 @@ class GamesThread(ThreadedComponent):
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}")
# 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)
logger.info(f"🎯 [EXTRACTION DEBUG] Step 2: Filtering result options to match fixture outcomes")
from ..database.models import ResultOptionModel, AvailableBetModel, ExtractionAssociationModel
......@@ -1909,6 +1965,12 @@ class GamesThread(ThreadedComponent):
self._collect_match_statistics(match_id, fixture_id, selected_result, session)
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
finally:
......@@ -1920,7 +1982,9 @@ class GamesThread(ThreadedComponent):
logger.error(f"❌ [EXTRACTION DEBUG] Full traceback: {traceback.format_exc()}")
# Fallback to 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:
"""Perform weighted random selection based on inverse coefficients"""
......@@ -1987,11 +2051,15 @@ class GamesThread(ThreadedComponent):
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"""
try:
logger.info(f"DEBUG _update_bet_results: Starting for match {match_id}, selected_result='{selected_result}'")
# Get coefficient for the selected result
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
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:
# UNDER/OVER bet wins
......@@ -2001,24 +2069,29 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result == 'pending'
).all()
logger.info(f"DEBUG _update_bet_results: Found {len(under_over_bets)} winning {under_over_outcome} bets")
for bet in under_over_bets:
win_amount = bet.amount * win_coefficient
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 = 'OVER' if under_over_outcome == 'UNDER' else 'UNDER'
session.query(BetDetailModel).filter(
losing_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == other_under_over,
BetDetailModel.result == 'pending'
).update({'result': 'lost'})
logger.info(f"DEBUG _update_bet_results: Set {losing_count} {other_under_over} bets to lost")
else:
# 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.outcome.in_(['UNDER', 'OVER']),
BetDetailModel.result == 'pending'
).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)
if selected_result not in ['UNDER', 'OVER']:
......@@ -2028,22 +2101,37 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result == 'pending'
).all()
logger.info(f"DEBUG _update_bet_results: Found {len(winning_bets)} winning {selected_result} bets")
for bet in winning_bets:
win_amount = bet.amount * win_coefficient
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'
session.query(BetDetailModel).filter(
losing_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending',
~BetDetailModel.outcome.in_([selected_result, 'UNDER', 'OVER'])
).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()
logger.info(f"Updated bet results for match {match_id}: winner={selected_result}, coefficient={win_coefficient}")
except Exception as 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()
def _collect_match_statistics(self, match_id: int, fixture_id: str, selected_result: str, session):
......@@ -2187,6 +2275,8 @@ class GamesThread(ThreadedComponent):
def _query_extracted_result(self, match_id: int) -> Optional[str]:
"""Query database for previously extracted result"""
try:
logger.info(f"DEBUG _query_extracted_result: Querying for match {match_id}")
session = self.db_manager.get_session()
try:
# Query for the most recent extraction stats for this match
......@@ -2195,6 +2285,13 @@ class GamesThread(ThreadedComponent):
ExtractionStatsModel.match_id == match_id
).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:
logger.info(f"Found extracted result for match {match_id}: {extraction_stats.actual_result}")
return extraction_stats.actual_result
......@@ -2207,6 +2304,8 @@ class GamesThread(ThreadedComponent):
except Exception as 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
def _determine_under_over_result(self, match_id: int, main_result: str) -> Optional[str]:
......@@ -2265,8 +2364,11 @@ class GamesThread(ThreadedComponent):
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:
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
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(
sender=self.name,
......@@ -2275,11 +2377,17 @@ class GamesThread(ThreadedComponent):
result=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)
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}")
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):
"""Send PLAY_VIDEO_RESULT message"""
......@@ -2299,17 +2407,25 @@ class GamesThread(ThreadedComponent):
def _send_match_done(self, fixture_id: str, match_id: int, result: str = None):
"""Send MATCH_DONE message"""
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(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id,
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)
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}")
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):
"""Send NEXT_MATCH message"""
......@@ -2353,6 +2469,9 @@ class GamesThread(ThreadedComponent):
try:
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
......@@ -2368,7 +2487,7 @@ class GamesThread(ThreadedComponent):
fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township,
start_time=now,
status='scheduled',
status=new_match_status,
fixture_id=fixture_id,
filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum,
......@@ -2376,7 +2495,11 @@ class GamesThread(ThreadedComponent):
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())
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)
......@@ -2391,11 +2514,11 @@ class GamesThread(ThreadedComponent):
)
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
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:
logger.error(f"Failed to create matches from old matches: {e}")
......@@ -2428,7 +2551,11 @@ class GamesThread(ThreadedComponent):
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())
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)
......@@ -2607,7 +2734,7 @@ class GamesThread(ThreadedComponent):
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"""
"""Select random completed matches from the database, excluding matches with same fighters as the last played match"""
try:
# Build query for completed matches
query = session.query(MatchModel).filter(
......@@ -2615,26 +2742,65 @@ class GamesThread(ThreadedComponent):
MatchModel.active_status == True
)
# Exclude the specified match if provided
# Exclude matches with same fighters as the last played match
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()
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
# 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})")
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 {exclude_match_id}: {e}")
logger.error(f"Failed to select random completed matches excluding same fighters: {e}")
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):
"""Clean up all previous unzipped match directories from temporary location"""
try:
......
......@@ -5,7 +5,8 @@ Server-side match timer component for synchronized countdown across all clients
import time
import logging
import threading
from typing import Dict, Any, Optional
import random
from typing import Dict, Any, Optional, List
from datetime import datetime, timedelta
from .thread_manager import ThreadedComponent
......@@ -396,17 +397,30 @@ class MatchTimerComponent(ThreadedComponent):
target_fixture_id = target_match.fixture_id
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
start_intro_message = MessageBuilder.start_intro(
sender=self.name,
fixture_id=target_fixture_id or target_match.fixture_id,
fixture_id=fixture_id,
match_id=target_match.id
)
self.message_bus.publish(start_intro_message)
return {
"fixture_id": target_fixture_id or target_match.fixture_id,
"fixture_id": fixture_id,
"match_id": target_match.id,
"match_number": target_match.match_number
}
......@@ -462,6 +476,197 @@ class MatchTimerComponent(ThreadedComponent):
logger.error(f"Failed to get match interval: {e}")
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):
"""Send timer update message to all clients"""
try:
......
......@@ -1749,6 +1749,7 @@ class PlayerWindow(QMainWindow):
self.loop_count = 0
self.current_loop_iteration = 0
self.current_file_path = None
logger.info("RESULT DEBUG: Initialized current_file_path = None")
# Match video tracking
self.is_playing_match_video = False
......@@ -1933,6 +1934,7 @@ class PlayerWindow(QMainWindow):
logger.info(f"QUrl path: {url.path()}")
# 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)
logger.info(f"Media player current state: {self.media_player.playbackState()}")
......@@ -2185,17 +2187,70 @@ class PlayerWindow(QMainWindow):
# 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
# Dynamically get result video filenames from database (excludes OVER/UNDER)
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"]
logger.info(f"RESULT DEBUG: Dynamically loaded result_videos = {result_videos}")
# DEBUG: Add comprehensive logging for result video end detection
logger.info(f"RESULT DEBUG: Checking for result video end detection")
logger.info(f"RESULT DEBUG: current_file_path = {self.current_file_path}")
logger.info(f"RESULT DEBUG: current_filename = {current_filename}")
# 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
else:
logger.info(f"RESULT DEBUG: Not a result video - continuing with normal flow")
# Handle loop functionality for intro videos
if self.loop_enabled:
......@@ -3128,21 +3183,6 @@ class QtVideoPlayer(QObject):
)
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:
logger.error(f"Failed to send progress update: {e}")
......@@ -4072,8 +4112,13 @@ class QtVideoPlayer(QObject):
)
# 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
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}")
except Exception as e:
......@@ -4150,56 +4195,130 @@ class QtVideoPlayer(QObject):
except Exception as 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"""
def _send_result_video_done_after_delay(self):
"""Send PLAY_VIDEO_RESULT_DONE message after the 5-second delay"""
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)
logger.info("RESULT DEBUG: _send_result_video_done_after_delay called - 5 second delay has elapsed")
# 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:
logger.info("RESULT DEBUG: current_result_info is not None, proceeding to send message")
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(
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')
fixture_id=fixture_id,
match_id=match_id,
result=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')}")
logger.info(f"RESULT DEBUG: Created PLAY_VIDEO_RESULT_DONE message: {done_message}")
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:
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):
"""Send MATCH_DONE message when result video is 3 seconds from ending"""
def _get_result_video_filenames(self) -> List[str]:
"""Dynamically get result video filenames from database, excluding OVER/UNDER"""
try:
if (hasattr(self, 'current_result_video_info') and
self.current_result_video_info):
# Get database manager from the QtVideoPlayer component
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}")
result_info = self.current_result_video_info
fixture_id = result_info['fixture_id']
match_id = result_info['match_id']
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()
except Exception as e:
logger.error(f"PlayerWindow: Failed to get result video filenames from database: {e}")
# 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
done_message = MessageBuilder.match_done(
done_message = MessageBuilder.play_video_result_done(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id
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 MATCH_DONE for fixture {fixture_id}, match {match_id}")
# Clear result video info
self.current_result_video_info = None
if hasattr(self, '_match_done_sent'):
delattr(self, '_match_done_sent')
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 match done message: {e}")
logger.error(f"Failed to send result video done message: {e}")
def _do_status_request(self, message: Message):
"""Execute status request on main thread"""
......
......@@ -53,10 +53,21 @@
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.1);
opacity: 0;
animation: fadeInScale 1s ease-out forwards;
opacity: 1; /* Always visible */
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 {
......@@ -632,11 +643,88 @@
padding: 20px;
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>
</head>
<body>
<div class="overlay-container">
<div class="results-panel" id="resultsPanel" style="display: flex;">
<div class="results-panel" id="resultsPanel">
<div class="results-content" id="resultsContent">
<!-- Title -->
<div class="results-title">RESULTS</div>
......@@ -647,20 +735,19 @@
</div>
</div>
<!-- Main Result -->
<div class="main-result-display" id="mainResultDisplay">
<div class="main-result-text" id="mainResultText">WIN1</div>
<!-- Combined Result Display -->
<div class="combined-result-display" id="combinedResultDisplay">
<div class="combined-result-text" id="combinedResultText">
<span id="mainResult">WIN1</span> / <span id="underOverResult">UNDER</span>
</div>
<!-- Under/Over Result -->
<div class="under-over-display" id="underOverDisplay">
<div class="under-over-text" id="underOverText">UNDER</div>
</div>
<!-- Winning Outcomes Grid -->
<div class="winning-outcomes-section">
<div class="outcomes-grid" id="outcomesGrid">
<!-- Winning outcomes will be populated here -->
<!-- Winning Bets Section -->
<div class="winning-bets-section">
<div class="bets-title">Winning bets:</div>
<div class="bets-list" id="betsList">
<!-- Winning bets will be populated here -->
</div>
</div>
</div>
</div>
......@@ -674,6 +761,11 @@
let currentMatch = null;
let winningOutcomes = [];
let animationStarted = false;
let videoStarted = false;
let contentVisible = false;
let videoStartTime = null;
let contentDelayTimer = null;
let resultsTimer = null;
// Outcome categories for styling
const outcomeCategories = {
......@@ -728,33 +820,34 @@
fetchWinningOutcomes(data.match_id);
}
// Start the animation sequence
startResultsAnimation();
// Prepare data but don't start animation yet - wait for video to actually start playing
console.log('Results data received, preparing animation data');
prepareResultsAnimation();
} else {
// No valid data, show loading state
showLoadingState();
}
}
// Start the results animation sequence
function startResultsAnimation() {
// Prepare the results animation sequence (called when data is received)
function prepareResultsAnimation() {
if (animationStarted) return;
animationStarted = true;
// Show results panel immediately
// Show results panel immediately (but content hidden)
showResultsPanel();
// Update fighters display
updateFightersDisplay();
// Update main result display
updateMainResultDisplay();
// Update combined result display
updateCombinedResultDisplay();
// Update under/over result display
updateUnderOverResultDisplay();
// Update winning bets display
updateWinningBetsDisplay();
// Update winning outcomes display
updateWinningOutcomesDisplay();
// Content will be shown after 5 seconds when video starts
}
// Fetch winning outcomes for the match
......@@ -768,7 +861,7 @@
const outcomesData = JSON.parse(outcomesJson);
console.log('Received winning outcomes:', outcomesData);
winningOutcomes = outcomesData || [];
updateWinningOutcomesDisplay();
updateWinningBetsDisplay();
} catch (error) {
console.error('Failed to get winning outcomes:', error);
// Fallback: show sample data for testing
......@@ -777,7 +870,7 @@
{ outcome: 'OVER', amount: 87.50 },
{ outcome: 'KO1', amount: 95.00 }
];
updateWinningOutcomesDisplay();
updateWinningBetsDisplay();
}
} else {
console.warn('Qt WebChannel not available for fetching winning outcomes');
......@@ -787,7 +880,7 @@
{ outcome: 'OVER', amount: 87.50 },
{ outcome: 'KO1', amount: 95.00 }
];
updateWinningOutcomesDisplay();
updateWinningBetsDisplay();
}
}
......@@ -797,6 +890,38 @@
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
function updateFightersDisplay() {
if (!currentMatch) {
......@@ -813,65 +938,63 @@
}
}
// Update main result display
function updateMainResultDisplay() {
const mainResultText = document.getElementById('mainResultText');
const mainResultDisplay = document.getElementById('mainResultDisplay');
// Update combined result display
function updateCombinedResultDisplay() {
const mainResultSpan = document.getElementById('mainResult');
const underOverSpan = document.getElementById('underOverResult');
const combinedDisplay = document.getElementById('combinedResultDisplay');
if (currentMainResult || currentUnderOverResult) {
if (currentMainResult) {
mainResultText.textContent = currentMainResult;
mainResultDisplay.style.display = 'block';
mainResultSpan.textContent = currentMainResult;
} else {
mainResultDisplay.style.display = 'none';
mainResultSpan.textContent = '';
}
}
// Update under/over result display
function updateUnderOverResultDisplay() {
const underOverText = document.getElementById('underOverText');
const underOverDisplay = document.getElementById('underOverDisplay');
if (currentUnderOverResult) {
underOverText.textContent = currentUnderOverResult;
underOverDisplay.className = `under-over-display ${currentUnderOverResult.toLowerCase()}`;
underOverDisplay.style.display = 'block';
underOverSpan.textContent = currentUnderOverResult;
} else {
underOverSpan.textContent = '';
}
combinedDisplay.style.display = 'block';
} else {
underOverDisplay.style.display = 'none';
combinedDisplay.style.display = 'none';
}
}
// Update winning outcomes display with staggered animation
function updateWinningOutcomesDisplay() {
const outcomesGrid = document.getElementById('outcomesGrid');
outcomesGrid.innerHTML = '';
// Update winning bets display with staggered animation
function updateWinningBetsDisplay() {
const betsList = document.getElementById('betsList');
betsList.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);
const noBetsDiv = document.createElement('div');
noBetsDiv.className = 'no-bets-message';
noBetsDiv.textContent = 'No winning bets for this match';
betsList.appendChild(noBetsDiv);
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) => {
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>
const betItem = document.createElement('div');
betItem.className = 'bet-item';
betItem.style.opacity = '0';
betItem.style.transform = 'translateY(20px)';
betItem.innerHTML = `
<div class="bet-outcome">${outcome.outcome || 'Unknown'}</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)
setTimeout(() => {
outcomeCard.style.transition = 'all 0.5s ease-out';
outcomeCard.style.opacity = '1';
outcomeCard.style.transform = 'translateY(0)';
betItem.style.transition = 'all 0.5s ease-out';
betItem.style.opacity = '1';
betItem.style.transform = 'translateY(0)';
}, index * 200);
});
}
......@@ -921,6 +1044,9 @@
// Always show results panel with default content
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)
......@@ -930,13 +1056,27 @@
// Connect to overlay object if available
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);
});
// 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
if (channel.objects.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) {
if (window.overlay.getCurrentData) {
window.overlay.getCurrentData(function(data) {
updateOverlayData(data);
});
}
......
......@@ -56,6 +56,70 @@ class DashboardAPI:
"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]:
"""Get video player status"""
try:
......
......@@ -805,6 +805,18 @@ def system_status():
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.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
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