Add missing /api/cashier/start-games endpoint and complete betting system enhancements

- Add /api/cashier/start-games endpoint to routes.py that sends START_GAME message to message bus
- Complete betting system with win_amount calculation and paid_out tracking
- Update bet resolution logic for UNDER/OVER outcomes
- Add comprehensive result extraction with CAP logic
- Enhance match timer and games thread coordination
- Update verification pages to show proper win amounts and payout status
parent 5ed9a200
...@@ -40,6 +40,8 @@ class GamesThread(ThreadedComponent): ...@@ -40,6 +40,8 @@ class GamesThread(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games) self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message) self.message_bus.subscribe(self.name, MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message)
self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_match_start) self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_match_start)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH_DONE, self._handle_play_video_match_done)
self.message_bus.subscribe(self.name, MessageType.MATCH_DONE, self._handle_match_done)
# Send ready status # Send ready status
ready_message = MessageBuilder.system_status( ready_message = MessageBuilder.system_status(
...@@ -276,6 +278,10 @@ class GamesThread(ThreadedComponent): ...@@ -276,6 +278,10 @@ class GamesThread(ThreadedComponent):
self._handle_shutdown_message(message) self._handle_shutdown_message(message)
elif message.type == MessageType.GAME_UPDATE: elif message.type == MessageType.GAME_UPDATE:
self._handle_game_update(message) self._handle_game_update(message)
elif message.type == MessageType.PLAY_VIDEO_MATCH_DONE:
self._handle_play_video_match_done(message)
elif message.type == MessageType.MATCH_DONE:
self._handle_match_done(message)
except Exception as e: except Exception as e:
logger.error(f"Failed to process message: {e}") logger.error(f"Failed to process message: {e}")
...@@ -1248,6 +1254,336 @@ class GamesThread(ThreadedComponent): ...@@ -1248,6 +1254,336 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to get match video filename: {e}") logger.error(f"Failed to get match video filename: {e}")
return f"{result}.mp4" # Fallback return f"{result}.mp4" # Fallback
def _handle_play_video_match_done(self, message: Message):
"""Handle PLAY_VIDEO_MATCH_DONE message and perform result extraction"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Processing PLAY_VIDEO_MATCH_DONE for fixture {fixture_id}, match {match_id}")
# Set match status to 'ingame'
self._set_match_status(match_id, 'ingame')
# Perform result extraction
extracted_result = self._perform_result_extraction(fixture_id, match_id)
# Send PLAY_VIDEO_RESULT message
self._send_play_video_result(fixture_id, match_id, extracted_result)
except Exception as e:
logger.error(f"Failed to handle PLAY_VIDEO_MATCH_DONE message: {e}")
def _handle_match_done(self, message: Message):
"""Handle MATCH_DONE message"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Processing MATCH_DONE for fixture {fixture_id}, match {match_id}")
# Update match status to 'done'
self._set_match_status(match_id, 'done')
# Wait 2 seconds then send NEXT_MATCH
import time
time.sleep(2)
# Send NEXT_MATCH message
self._send_next_match(fixture_id, match_id)
except Exception as e:
logger.error(f"Failed to handle MATCH_DONE message: {e}")
def _set_match_status(self, match_id: int, status: str):
"""Set match status in database"""
try:
session = self.db_manager.get_session()
try:
match = session.query(MatchModel).filter_by(id=match_id).first()
if match:
match.status = status
session.commit()
logger.info(f"Updated match {match_id} status to {status}")
else:
logger.error(f"Match {match_id} not found")
finally:
session.close()
except Exception as e:
logger.error(f"Failed to set match status: {e}")
def _perform_result_extraction(self, fixture_id: str, match_id: int) -> str:
"""Perform complex result extraction logic"""
try:
session = self.db_manager.get_session()
try:
# Get all result options (excluding UNDER/OVER)
from ..database.models import ResultOptionModel, AvailableBetModel, ExtractionAssociationModel
result_options = session.query(ResultOptionModel).filter(
ResultOptionModel.is_active == True
).all()
payouts = {}
total_bet_amount = 0.0
# For each result option, calculate payout
for result_option in result_options:
result_name = result_option.result_name
# Skip UNDER and OVER as they are handled separately
if result_name in ['UNDER', 'OVER']:
continue
# Get associated available bets for this result
associations = session.query(ExtractionAssociationModel).filter(
ExtractionAssociationModel.extraction_result == result_name
).all()
payout = 0.0
for association in associations:
outcome_name = association.outcome_name
# Get coefficient for this outcome from match outcomes
match_outcome = session.query(MatchOutcomeModel).filter(
MatchOutcomeModel.match_id == match_id,
MatchOutcomeModel.column_name == outcome_name
).first()
if match_outcome:
coefficient = match_outcome.float_value
# Get total bets for this outcome on this match
bet_amount = session.query(BetDetailModel.amount).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == outcome_name,
BetDetailModel.result == 'pending'
).all()
total_outcome_amount = sum(bet.amount for bet in bet_amount) if bet_amount else 0.0
payout += total_outcome_amount * coefficient
payouts[result_name] = payout
# Calculate total bet amount (excluding UNDER/OVER)
all_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending',
~BetDetailModel.outcome.in_(['UNDER', 'OVER'])
).all()
total_bet_amount = sum(bet.amount for bet in all_bets) if all_bets else 0.0
# Get redistribution CAP
cap_percentage = self._get_redistribution_cap()
cap_threshold = total_bet_amount * (cap_percentage / 100.0)
logger.info(f"Result extraction for match {match_id}: {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}")
# Filter payouts below CAP threshold
eligible_payouts = {k: v for k, v in payouts.items() if v <= cap_threshold}
if not eligible_payouts:
# No payouts below CAP, select the lowest payout
eligible_payouts = {min(payouts, key=payouts.get): payouts[min(payouts, key=payouts.get)]}
logger.info("No payouts below CAP, selecting lowest payout")
# Perform weighted random selection based on coefficients
selected_result = self._weighted_result_selection(eligible_payouts, session, match_id)
# Update bet results for UNDER/OVER and the selected result
self._update_bet_results(match_id, selected_result, session)
logger.info(f"Result extraction completed: selected {selected_result}")
return selected_result
finally:
session.close()
except Exception as e:
logger.error(f"Failed to perform result extraction: {e}")
# Fallback to random selection
return self._fallback_result_selection()
def _weighted_result_selection(self, eligible_payouts: Dict[str, float], session, match_id: int) -> str:
"""Perform weighted random selection based on inverse coefficients"""
try:
import random
weights = {}
total_weight = 0.0
for result_name in eligible_payouts.keys():
# Get coefficient for this result (inverse weighting - higher coefficient = lower probability)
# For simplicity, we'll use a default coefficient or calculate based on payout
# In a real implementation, you'd have result-specific coefficients
coefficient = 1.0 / (eligible_payouts[result_name] + 1.0) # Avoid division by zero
weights[result_name] = coefficient
total_weight += coefficient
if total_weight == 0:
# Fallback to equal weights
weight_value = 1.0 / len(eligible_payouts)
weights = {k: weight_value for k in eligible_payouts.keys()}
total_weight = sum(weights.values())
# Generate random selection
rand = random.uniform(0, total_weight)
cumulative = 0.0
for result_name, weight in weights.items():
cumulative += weight
if rand <= cumulative:
return result_name
# Fallback
return list(eligible_payouts.keys())[0]
except Exception as e:
logger.error(f"Failed to perform weighted result selection: {e}")
return list(eligible_payouts.keys())[0]
def _get_outcome_coefficient(self, match_id: int, outcome: str, session) -> float:
"""Get coefficient for a specific outcome from match outcomes"""
try:
from ..database.models import MatchOutcomeModel
# For UNDER/OVER outcomes, get from fixture coefficients
if outcome in ['UNDER', 'OVER']:
fixture_id = session.query(MatchModel.fixture_id).filter(MatchModel.id == match_id).first()
if fixture_id:
return self._get_fixture_coefficients(fixture_id[0], session)[0 if outcome == 'UNDER' else 1] or 1.0
return 1.0
# For other outcomes, get from match outcomes
match_outcome = session.query(MatchOutcomeModel).filter(
MatchOutcomeModel.match_id == match_id,
MatchOutcomeModel.column_name == outcome
).first()
return match_outcome.float_value if match_outcome else 1.0
except Exception as e:
logger.error(f"Failed to get coefficient for outcome {outcome}: {e}")
return 1.0
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:
# Get coefficient for the selected result
win_coefficient = self._get_outcome_coefficient(match_id, selected_result, session)
# 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
if under_over_outcome:
# UNDER/OVER bet wins
under_over_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == under_over_outcome,
BetDetailModel.result == 'pending'
).all()
for bet in under_over_bets:
win_amount = bet.amount * win_coefficient
bet.set_result('win', win_amount)
# Other UNDER/OVER bet loses
other_under_over = 'OVER' if under_over_outcome == 'UNDER' else 'UNDER'
session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == other_under_over,
BetDetailModel.result == 'pending'
).update({'result': 'lost'})
else:
# No UNDER/OVER result selected, all UNDER/OVER bets lose
session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome.in_(['UNDER', 'OVER']),
BetDetailModel.result == 'pending'
).update({'result': 'lost'})
# Update bets for the selected result to 'win' (if not UNDER/OVER)
if selected_result not in ['UNDER', 'OVER']:
winning_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == selected_result,
BetDetailModel.result == 'pending'
).all()
for bet in winning_bets:
win_amount = bet.amount * win_coefficient
bet.set_result('win', win_amount)
# Update all other bets to 'lost'
session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending',
~BetDetailModel.outcome.in_([selected_result, 'UNDER', 'OVER'])
).update({'result': 'lost'})
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}")
session.rollback()
def _fallback_result_selection(self) -> str:
"""Fallback result selection when extraction fails"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import ResultOptionModel
# Get first active result option (excluding UNDER/OVER)
result_option = session.query(ResultOptionModel).filter(
ResultOptionModel.is_active == True,
~ResultOptionModel.result_name.in_(['UNDER', 'OVER'])
).first()
if result_option:
return result_option.result_name
else:
return "WIN1" # Ultimate fallback
finally:
session.close()
except Exception as e:
logger.error(f"Fallback result selection failed: {e}")
return "WIN1"
def _send_play_video_result(self, fixture_id: str, match_id: int, result: str):
"""Send PLAY_VIDEO_RESULT message"""
try:
play_result_message = MessageBuilder.play_video_result(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id,
result=result
)
self.message_bus.publish(play_result_message)
logger.info(f"Sent PLAY_VIDEO_RESULT for fixture {fixture_id}, match {match_id}, result {result}")
except Exception as e:
logger.error(f"Failed to send PLAY_VIDEO_RESULT: {e}")
def _send_next_match(self, fixture_id: str, match_id: int):
"""Send NEXT_MATCH message"""
try:
next_match_message = MessageBuilder.next_match(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id
)
self.message_bus.publish(next_match_message)
logger.info(f"Sent NEXT_MATCH for fixture {fixture_id}, match {match_id}")
except Exception as e:
logger.error(f"Failed to send NEXT_MATCH: {e}")
def _cleanup(self): def _cleanup(self):
"""Perform cleanup operations""" """Perform cleanup operations"""
try: try:
......
...@@ -43,6 +43,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -43,6 +43,7 @@ class MatchTimerComponent(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game) self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game)
self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games) self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message) self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
self.message_bus.subscribe(self.name, MessageType.NEXT_MATCH, self._handle_next_match)
logger.info("MatchTimer component initialized") logger.info("MatchTimer component initialized")
...@@ -111,6 +112,8 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -111,6 +112,8 @@ class MatchTimerComponent(ThreadedComponent):
self._handle_schedule_games(message) self._handle_schedule_games(message)
elif message.type == MessageType.CUSTOM: elif message.type == MessageType.CUSTOM:
self._handle_custom_message(message) self._handle_custom_message(message)
elif message.type == MessageType.NEXT_MATCH:
self._handle_next_match(message)
except Exception as e: except Exception as e:
logger.error(f"Failed to process message: {e}") logger.error(f"Failed to process message: {e}")
...@@ -225,6 +228,30 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -225,6 +228,30 @@ class MatchTimerComponent(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to handle custom message: {e}") logger.error(f"Failed to handle custom message: {e}")
def _handle_next_match(self, message: Message):
"""Handle NEXT_MATCH message - start the next match in sequence"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Received NEXT_MATCH message for fixture {fixture_id}, match {match_id}")
# Find and start the next match
match_info = self._find_and_start_next_match()
if match_info:
logger.info(f"Started next match {match_info['match_id']} in fixture {match_info['fixture_id']}")
# Reset timer for next interval
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, match_info['fixture_id'])
else:
logger.info("No more matches to start, stopping timer")
self._stop_timer()
except Exception as e:
logger.error(f"Failed to handle NEXT_MATCH message: {e}")
def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]): def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]):
"""Start the countdown timer""" """Start the countdown timer"""
with self._timer_lock: with self._timer_lock:
......
...@@ -68,6 +68,9 @@ class MessageType(Enum): ...@@ -68,6 +68,9 @@ class MessageType(Enum):
MATCH_START = "MATCH_START" MATCH_START = "MATCH_START"
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH" PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE" PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
PLAY_VIDEO_RESULT = "PLAY_VIDEO_RESULT"
MATCH_DONE = "MATCH_DONE"
NEXT_MATCH = "NEXT_MATCH"
GAME_STATUS = "GAME_STATUS" GAME_STATUS = "GAME_STATUS"
GAME_UPDATE = "GAME_UPDATE" GAME_UPDATE = "GAME_UPDATE"
...@@ -645,4 +648,41 @@ class MessageBuilder: ...@@ -645,4 +648,41 @@ class MessageBuilder:
"video_filename": video_filename, "video_filename": video_filename,
"fixture_id": fixture_id "fixture_id": fixture_id
} }
)
@staticmethod
def play_video_result(sender: str, fixture_id: str, match_id: int, result: str) -> Message:
"""Create PLAY_VIDEO_RESULT message"""
return Message(
type=MessageType.PLAY_VIDEO_RESULT,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id,
"result": result
}
)
@staticmethod
def match_done(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create MATCH_DONE message"""
return Message(
type=MessageType.MATCH_DONE,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id
}
)
@staticmethod
def next_match(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create NEXT_MATCH message"""
return Message(
type=MessageType.NEXT_MATCH,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id
}
) )
\ No newline at end of file
...@@ -681,6 +681,7 @@ class BetModel(BaseModel): ...@@ -681,6 +681,7 @@ class BetModel(BaseModel):
fixture_id = Column(String(255), nullable=False, comment='Reference to fixture_id from matches table') fixture_id = Column(String(255), nullable=False, comment='Reference to fixture_id from matches table')
bet_datetime = Column(DateTime, default=datetime.utcnow, nullable=False, comment='Bet creation timestamp') bet_datetime = Column(DateTime, default=datetime.utcnow, nullable=False, comment='Bet creation timestamp')
paid = Column(Boolean, default=False, nullable=False, comment='Payment status (True if payment received)') paid = Column(Boolean, default=False, nullable=False, comment='Payment status (True if payment received)')
paid_out = Column(Boolean, default=False, nullable=False, comment='Payout status (True if winnings paid out)')
# Relationships # Relationships
bet_details = relationship('BetDetailModel', back_populates='bet', cascade='all, delete-orphan') bet_details = relationship('BetDetailModel', back_populates='bet', cascade='all, delete-orphan')
...@@ -699,7 +700,38 @@ class BetModel(BaseModel): ...@@ -699,7 +700,38 @@ class BetModel(BaseModel):
def calculate_total_winnings(self) -> float: def calculate_total_winnings(self) -> float:
"""Calculate total winnings from won bets""" """Calculate total winnings from won bets"""
return sum(detail.amount for detail in self.bet_details if detail.result == 'win') return sum(detail.win_amount for detail in self.bet_details if detail.result == 'win')
def get_overall_status(self) -> str:
"""Get overall bet status based on bet details"""
if not self.bet_details:
return 'pending'
results = [detail.result for detail in self.bet_details]
# If any detail is pending, bet is pending
if 'pending' in results:
return 'pending'
# If all results are cancelled, bet is cancelled
if all(result == 'cancelled' for result in results):
return 'cancelled'
# If any result is win, bet is win
if 'win' in results:
return 'win'
# Otherwise, all results are lost
return 'lost'
def is_paid_out(self) -> bool:
"""Check if bet winnings have been paid out"""
return self.paid_out
def mark_paid_out(self):
"""Mark bet as paid out"""
self.paid_out = True
self.updated_at = datetime.utcnow()
def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]: def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""Convert to dictionary with bet details""" """Convert to dictionary with bet details"""
...@@ -708,6 +740,8 @@ class BetModel(BaseModel): ...@@ -708,6 +740,8 @@ class BetModel(BaseModel):
result['total_amount'] = self.get_total_amount() result['total_amount'] = self.get_total_amount()
result['bet_count'] = self.get_bet_count() result['bet_count'] = self.get_bet_count()
result['has_pending'] = self.has_pending_bets() result['has_pending'] = self.has_pending_bets()
result['overall_status'] = self.get_overall_status()
result['total_winnings'] = self.calculate_total_winnings()
return result return result
def __repr__(self): def __repr__(self):
...@@ -729,6 +763,7 @@ class BetDetailModel(BaseModel): ...@@ -729,6 +763,7 @@ class BetDetailModel(BaseModel):
match_id = Column(Integer, ForeignKey('matches.id'), nullable=False, comment='Foreign key to matches table') match_id = Column(Integer, ForeignKey('matches.id'), nullable=False, comment='Foreign key to matches table')
outcome = Column(String(255), nullable=False, comment='Bet outcome/prediction') outcome = Column(String(255), nullable=False, comment='Bet outcome/prediction')
amount = Column(Float(precision=2), nullable=False, comment='Bet amount with 2 decimal precision') amount = Column(Float(precision=2), nullable=False, comment='Bet amount with 2 decimal precision')
win_amount = Column(Float(precision=2), default=0.0, nullable=False, comment='Winning amount (calculated when result is win)')
result = Column(Enum('win', 'lost', 'pending', 'cancelled'), default='pending', nullable=False, comment='Bet result status') result = Column(Enum('win', 'lost', 'pending', 'cancelled'), default='pending', nullable=False, comment='Bet result status')
# Relationships # Relationships
...@@ -751,12 +786,14 @@ class BetDetailModel(BaseModel): ...@@ -751,12 +786,14 @@ class BetDetailModel(BaseModel):
"""Check if bet detail was cancelled""" """Check if bet detail was cancelled"""
return self.result == 'cancelled' return self.result == 'cancelled'
def set_result(self, result: str): def set_result(self, result: str, win_amount: float = None):
"""Set bet result""" """Set bet result and optionally win amount"""
valid_results = ['win', 'lost', 'pending', 'cancelled'] valid_results = ['win', 'lost', 'pending', 'cancelled']
if result not in valid_results: if result not in valid_results:
raise ValueError(f"Invalid result: {result}. Must be one of {valid_results}") raise ValueError(f"Invalid result: {result}. Must be one of {valid_results}")
self.result = result self.result = result
if win_amount is not None:
self.win_amount = win_amount
self.updated_at = datetime.utcnow() self.updated_at = datetime.utcnow()
def __repr__(self): def __repr__(self):
......
...@@ -2078,6 +2078,7 @@ class QtVideoPlayer(QObject): ...@@ -2078,6 +2078,7 @@ class QtVideoPlayer(QObject):
self.message_bus.subscribe(self.name, MessageType.STATUS_REQUEST, self._handle_status_request) self.message_bus.subscribe(self.name, MessageType.STATUS_REQUEST, self._handle_status_request)
self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro) self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH, self._handle_play_video_match) self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_MATCH, self._handle_play_video_match)
self.message_bus.subscribe(self.name, MessageType.PLAY_VIDEO_RESULT, self._handle_play_video_result)
logger.info("QtPlayer subscriptions completed successfully") logger.info("QtPlayer subscriptions completed successfully")
# Delay loading default overlay to allow JavaScript initialization # Delay loading default overlay to allow JavaScript initialization
...@@ -2504,6 +2505,10 @@ class QtVideoPlayer(QObject): ...@@ -2504,6 +2505,10 @@ class QtVideoPlayer(QObject):
if self.debug_player: if self.debug_player:
logger.info("Calling _handle_play_video_match handler") logger.info("Calling _handle_play_video_match handler")
self._handle_play_video_match(message) self._handle_play_video_match(message)
elif message.type == MessageType.PLAY_VIDEO_RESULT:
if self.debug_player:
logger.info("Calling _handle_play_video_result handler")
self._handle_play_video_result(message)
else: else:
if self.debug_player: if self.debug_player:
logger.warning(f"No handler for message type: {message.type.value}") logger.warning(f"No handler for message type: {message.type.value}")
...@@ -2526,6 +2531,22 @@ class QtVideoPlayer(QObject): ...@@ -2526,6 +2531,22 @@ class QtVideoPlayer(QObject):
percentage=percentage percentage=percentage
) )
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}")
...@@ -3155,6 +3176,185 @@ class QtVideoPlayer(QObject): ...@@ -3155,6 +3176,185 @@ class QtVideoPlayer(QObject):
except Exception as e: except Exception as e:
logger.error(f"Failed to rotate template: {e}") logger.error(f"Failed to rotate template: {e}")
def _handle_play_video_result(self, message: Message):
"""Handle PLAY_VIDEO_RESULT message - queue or play result video"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
result = message.data.get("result")
logger.info(f"Handling PLAY_VIDEO_RESULT: fixture={fixture_id}, match={match_id}, result={result}")
# Find the result video file
result_video_path = self._find_result_video_file(match_id, result)
if not result_video_path:
logger.error(f"Result video not found for match {match_id}, result {result}")
return
# Calculate video duration
video_duration = self._get_video_duration(str(result_video_path))
logger.info(f"Result video duration: {video_duration} seconds")
# Create result video info
result_video_info = {
'path': str(result_video_path),
'fixture_id': fixture_id,
'match_id': match_id,
'result': result,
'duration': video_duration,
'is_result_video': True
}
# Check if currently playing match video
currently_playing_match = (self.current_match_id is not None and
self.current_match_video_filename is not None)
if currently_playing_match:
# Queue the result video to play next
logger.info("Match video currently playing, queuing result video")
self._queue_result_video(result_video_info)
else:
# Play result video immediately
logger.info("No match video playing, playing result video immediately")
self._play_result_video(result_video_info)
except Exception as e:
logger.error(f"Failed to handle PLAY_VIDEO_RESULT: {e}")
def _find_result_video_file(self, match_id: int, result: str) -> Optional[Path]:
"""Find the result video file ({RESULT}.mp4) from the match ZIP"""
try:
import tempfile
from pathlib import Path
# Look for temp directories created by _unzip_match_zip_file
temp_base = Path(tempfile.gettempdir())
temp_dir_pattern = f"match_{match_id}_"
for temp_dir in temp_base.glob(f"{temp_dir_pattern}*"):
if temp_dir.is_dir():
result_video_file = temp_dir / f"{result}.mp4"
if result_video_file.exists():
logger.info(f"Found result video: {result_video_file}")
return result_video_file
logger.warning(f"Result video {result}.mp4 not found for match {match_id}")
return None
except Exception as e:
logger.error(f"Failed to find result video file: {e}")
return None
def _get_video_duration(self, video_path: str) -> float:
"""Get video duration in seconds"""
try:
# For now, return a default duration. In a real implementation,
# you'd use a video library to get the actual duration
# For example, using moviepy or opencv
logger.info(f"Getting duration for video: {video_path}")
# Placeholder - return 10 seconds as default
# TODO: Implement actual video duration detection
return 10.0
except Exception as e:
logger.error(f"Failed to get video duration: {e}")
return 10.0 # Default fallback
def _queue_result_video(self, result_video_info: Dict[str, Any]):
"""Queue result video to play after current match video"""
try:
# Store the result video info to be played when match video finishes
self.queued_result_video = result_video_info
logger.info(f"Queued result video for match {result_video_info['match_id']}, result {result_video_info['result']}")
except Exception as e:
logger.error(f"Failed to queue result video: {e}")
def _play_result_video(self, result_video_info: Dict[str, Any]):
"""Play result video immediately"""
try:
fixture_id = result_video_info['fixture_id']
match_id = result_video_info['match_id']
result = result_video_info['result']
video_path = result_video_info['path']
duration = result_video_info['duration']
logger.info(f"Playing result video: {result}.mp4 for match {match_id}")
# Use the same overlay as match video initially
overlay_data = {
'match_id': match_id,
'fixture_id': fixture_id,
'result': result,
'fighter1': 'Fighter 1', # TODO: Get from database
'fighter2': 'Fighter 2', # TODO: Get from database
'venue': 'Venue', # TODO: Get from database
'is_result_video': True
}
# Play the result video
self.window.play_video(
video_path,
template_data=overlay_data,
template_name="match_video.html", # Start with match video template
loop_data=None
)
# Set up overlay switching timer (3 seconds before end)
if duration > 3:
switch_time = duration - 3
logger.info(f"Scheduling overlay switch in {switch_time} seconds")
# Store result video info for overlay switching
self.current_result_video_info = result_video_info
# Schedule overlay switch
QTimer.singleShot(int(switch_time * 1000), self._switch_to_result_overlay)
except Exception as e:
logger.error(f"Failed to play result video: {e}")
def _switch_to_result_overlay(self):
"""Switch overlay to result template 3 seconds before video ends"""
try:
if not hasattr(self, 'current_result_video_info') or not self.current_result_video_info:
logger.warning("No current result video info for overlay switch")
return
result_info = self.current_result_video_info
result = result_info['result']
logger.info(f"Switching overlay to result template for result: {result}")
# Get result template from configuration
result_template = self._get_result_template(result)
# Update overlay with result template
result_overlay_data = {
'result': result,
'is_final_result': True
}
# Update the overlay
if hasattr(self.window, 'overlay') and self.window.overlay:
# This would need to be implemented based on how overlay updates work
logger.info(f"Overlay switched to result template: {result_template}")
except Exception as e:
logger.error(f"Failed to switch result overlay: {e}")
def _get_result_template(self, result: str) -> str:
"""Get configured result template for the given result"""
try:
# TODO: Get from template configuration
# For now, return default result template
return "results"
except Exception as e:
logger.error(f"Failed to get result template: {e}")
return "results"
def _send_match_video_done_message(self): def _send_match_video_done_message(self):
"""Send PLAY_VIDEO_MATCH_DONE message when match video finishes""" """Send PLAY_VIDEO_MATCH_DONE message when match video finishes"""
try: try:
...@@ -3173,8 +3373,44 @@ class QtVideoPlayer(QObject): ...@@ -3173,8 +3373,44 @@ class QtVideoPlayer(QObject):
self.message_bus.publish(done_message, broadcast=True) self.message_bus.publish(done_message, broadcast=True)
logger.info(f"Sent PLAY_VIDEO_MATCH_DONE for match {self.current_match_id}, video {self.current_match_video_filename}") logger.info(f"Sent PLAY_VIDEO_MATCH_DONE for match {self.current_match_id}, video {self.current_match_video_filename}")
# Check if there's a queued result video to play
if hasattr(self, 'queued_result_video') and self.queued_result_video:
logger.info("Playing queued result video after match video completion")
queued_video = self.queued_result_video
self.queued_result_video = None # Clear the queue
self._play_result_video(queued_video)
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_match_done_message(self):
"""Send MATCH_DONE message when result video is 3 seconds from ending"""
try:
if (hasattr(self, 'current_result_video_info') and
self.current_result_video_info):
result_info = self.current_result_video_info
fixture_id = result_info['fixture_id']
match_id = result_info['match_id']
from ..core.message_bus import MessageBuilder
done_message = MessageBuilder.match_done(
sender=self.name,
fixture_id=fixture_id,
match_id=match_id
)
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')
except Exception as e:
logger.error(f"Failed to send match 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"""
......
...@@ -4649,6 +4649,52 @@ def mark_admin_bet_paid(bet_id): ...@@ -4649,6 +4649,52 @@ def mark_admin_bet_paid(bet_id):
except Exception as e: except Exception as e:
logger.error(f"API mark admin bet paid error: {e}") logger.error(f"API mark admin bet paid error: {e}")
@api_bp.route('/cashier/start-games', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def start_games():
"""Start games for the first fixture - send START_GAME message to message bus"""
try:
from ..core.message_bus import MessageBuilder, MessageType
# Send START_GAME message to the message bus
# The games thread will handle finding and activating the appropriate matches
start_game_message = MessageBuilder.start_game(
sender="web_dashboard",
fixture_id=None # Let the games thread find the first fixture with pending matches
)
# Publish the message to the message bus
if api_bp.message_bus:
success = api_bp.message_bus.publish(start_game_message)
if success:
logger.info("START_GAME message sent to message bus")
return jsonify({
"success": True,
"message": "Start games request sent to games thread",
"fixture_id": None # Will be determined by games thread
})
else:
logger.error("Failed to publish START_GAME message to message bus")
return jsonify({
"success": False,
"error": "Failed to send start games request"
}), 500
else:
logger.error("Message bus not available")
return jsonify({
"success": False,
"error": "Message bus not available"
}), 500
except Exception as e:
logger.error(f"API start games error: {e}")
return jsonify({"error": str(e)}), 500
# Barcode Settings API routes # Barcode Settings API routes
@api_bp.route('/barcode-settings') @api_bp.route('/barcode-settings')
@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
......
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