diff --git a/tournaments/models/match.py b/tournaments/models/match.py index 08a4b42..856f13d 100644 --- a/tournaments/models/match.py +++ b/tournaments/models/match.py @@ -423,6 +423,37 @@ class Match(TournamentSubModel): return timezone.now() > self.start_date return False + def will_start(self): + """ + Returns True if match will start within the next hour + """ + if self.disabled or self.end_date: + return False + + # Check start_date first + if self.start_date: + now = timezone.now() + time_until_start = (self.start_date - now).total_seconds() + return 0 < time_until_start <= 3600 # Within 1 hour (3600 seconds) + + # Check planned_start_date as fallback + if self.planned_start_date: + now = timezone.now() + time_until_start = (self.planned_start_date - now).total_seconds() + return 0 < time_until_start <= 3600 # Within 1 hour (3600 seconds) + + return False + + def is_ongoing(self): + """ + Returns True if match has started but not finished + """ + if self.disabled: + return False + + # Match is ongoing if it has started but hasn't ended + return self.started() and self.end_date is None + def should_appear(self): if self.disabled is True: return False diff --git a/tournaments/models/tournament.py b/tournaments/models/tournament.py index a9a0566..f94885f 100644 --- a/tournaments/models/tournament.py +++ b/tournaments/models/tournament.py @@ -1031,6 +1031,72 @@ class Tournament(BaseModel): def will_start_soon(self, hour_delta=2): return self.has_started(hour_delta=hour_delta) + def has_ongoing_matches(self): + """ + Returns True if tournament has any matches that are currently ongoing + """ + # Check matches in rounds + for round_obj in self.rounds.all(): + for match in round_obj.matches.all(): + if match.is_ongoing(): + return True + + # Check matches in group stages + for group_stage in self.group_stages.all(): + for match in group_stage.matches.all(): + if match.is_ongoing(): + return True + + return False + + def has_matches_starting_soon(self): + """ + Returns True if tournament has any matches starting within the next hour + """ + # Check matches in rounds + for round_obj in self.rounds.all(): + for match in round_obj.matches.all(): + if match.will_start(): + return True + + # Check matches in group stages + for group_stage in self.group_stages.all(): + for match in group_stage.matches.all(): + if match.will_start(): + return True + + return False + + def get_next_match_start_time(self): + """ + Returns the datetime of the earliest upcoming match, or None if no upcoming matches + """ + next_times = [] + + # Check matches in rounds + for round_obj in self.rounds.all(): + for match in round_obj.matches.all(): + if match.disabled or match.end_date: + continue + + if match.start_date and match.start_date > timezone.now(): + next_times.append(match.start_date) + elif match.planned_start_date and match.planned_start_date > timezone.now(): + next_times.append(match.planned_start_date) + + # Check matches in group stages + for group_stage in self.group_stages.all(): + for match in group_stage.matches.all(): + if match.disabled or match.end_date: + continue + + if match.start_date and match.start_date > timezone.now(): + next_times.append(match.start_date) + elif match.planned_start_date and match.planned_start_date > timezone.now(): + next_times.append(match.planned_start_date) + + return min(next_times) if next_times else None + def are_teams_positioned(self): teams = self.team_registrations.all() filtered_teams = [t for t in teams if t.is_positioned()] diff --git a/tournaments/views.py b/tournaments/views.py index 3e392e0..4dfcdea 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -534,28 +534,24 @@ def automatic_broadcast_event(request, event_id): event = get_object_or_404(Event, pk=event_id) now = timezone.now() - # Initial time windows - past_cutoff = now - timedelta(hours=4) - future_cutoff = now + timedelta(hours=4) + # Get all tournaments for the event (expanded time range for better coverage) + past_cutoff = now - timedelta(hours=12) # Extended past search + future_cutoff = now + timedelta(hours=12) # Extended future search - # Get recent/current tournaments (started but not too old) - recent_tournaments = Tournament.objects.filter( + all_tournaments = Tournament.objects.filter( event=event, start_date__gte=past_cutoff, - start_date__lte=now - ).order_by('start_date') - - # Get near-future tournaments - future_tournaments = Tournament.objects.filter( - event=event, - start_date__gt=now, start_date__lte=future_cutoff ).order_by('start_date') - # Filter recent tournaments that have content - recent_with_content = [] - for tournament in recent_tournaments: + # Categorize tournaments by priority + ongoing_tournaments = [] + starting_soon_tournaments = [] + other_tournaments_with_content = [] + + for tournament in all_tournaments: try: + # First check if tournament has broadcast content content = tournament.broadcast_content() has_content = ( len(content.get('matches', [])) > 0 or @@ -563,59 +559,92 @@ def automatic_broadcast_event(request, event_id): len(content.get('summons', [])) > 0 or len(content.get('rankings', [])) > 0 ) + if has_content: - recent_with_content.append(tournament) - except: + # Categorize based on match activity (highest priority first) + if tournament.has_ongoing_matches(): + ongoing_tournaments.append(tournament) + elif tournament.has_matches_starting_soon(): + starting_soon_tournaments.append(tournament) + else: + other_tournaments_with_content.append(tournament) + except Exception as e: + print(f"Error processing tournament {tournament.id}: {e}") continue - tournaments_to_display = recent_with_content[:] + # Sort each category by next match start time and tournament start time + def sort_by_next_match_time(tournament): + next_match_time = tournament.get_next_match_start_time() + return ( + next_match_time if next_match_time else tournament.start_date, + tournament.start_date + ) - # If we have 0 or 1 recent tournaments, extend the search - if len(recent_with_content) <= 1: - needed = 2 - len(recent_with_content) + # Sort ongoing tournaments by next match time (for consistency) + ongoing_tournaments.sort(key=sort_by_next_match_time) - # First, try to add future tournaments (within 4 hours) - added_from_future = 0 - for tournament in future_tournaments: - if added_from_future >= needed: - break - tournaments_to_display.append(tournament) - added_from_future += 1 + # Sort starting soon tournaments by next match time (earliest first) + starting_soon_tournaments.sort(key=sort_by_next_match_time) - # If still not enough, extend past search (beyond 4 hours) - if len(tournaments_to_display) < 2: - extended_past_tournaments = Tournament.objects.filter( - event=event, - start_date__lt=past_cutoff # Older than 4 hours ago - ).order_by('-start_date') # Most recent first - - still_needed = 2 - len(tournaments_to_display) - for tournament in extended_past_tournaments[:still_needed]: - # Check if this past tournament has content - try: - content = tournament.broadcast_content() - has_content = ( - len(content.get('matches', [])) > 0 or - len(content.get('group_stages', [])) > 0 or - len(content.get('summons', [])) > 0 or - len(content.get('rankings', [])) > 0 - ) - if has_content: - tournaments_to_display.append(tournament) - except: - continue - - # If still not enough, extend future search (beyond 4 hours) - if len(tournaments_to_display) < 2: - extended_future_tournaments = Tournament.objects.filter( - event=event, - start_date__gt=future_cutoff # More than 4 hours in the future - ).order_by('start_date') + # Sort other tournaments by tournament start time + other_tournaments_with_content.sort(key=lambda t: t.start_date) - still_needed = 2 - len(tournaments_to_display) - for tournament in extended_future_tournaments[:still_needed]: + # Build final list prioritizing ongoing matches above all else + tournaments_to_display = [] + + # Priority 1: Tournaments with ongoing matches (take max 2) + tournaments_to_display.extend(ongoing_tournaments[:2]) + + # If we need more tournaments and have space + if len(tournaments_to_display) < 2: + remaining_slots = 2 - len(tournaments_to_display) + + # Priority 2: Tournaments with matches starting soon + tournaments_to_display.extend(starting_soon_tournaments[:remaining_slots]) + + # If still need more tournaments + if len(tournaments_to_display) < 2: + remaining_slots = 2 - len(tournaments_to_display) + + # Priority 3: Other tournaments with content + tournaments_to_display.extend(other_tournaments_with_content[:remaining_slots]) + + # Fallback: If still don't have enough tournaments, expand search + if len(tournaments_to_display) < 2: + # Get tournaments from wider time range + extended_tournaments = Tournament.objects.filter( + event=event + ).exclude( + id__in=[t.id for t in tournaments_to_display] + ).order_by('-start_date') # Most recent first + + remaining_needed = 2 - len(tournaments_to_display) + for tournament in extended_tournaments[:remaining_needed]: + try: + # Check if tournament has any content + content = tournament.broadcast_content() + has_content = ( + len(content.get('matches', [])) > 0 or + len(content.get('group_stages', [])) > 0 or + len(content.get('summons', [])) > 0 or + len(content.get('rankings', [])) > 0 + ) + if has_content: + tournaments_to_display.append(tournament) + except: + # If no content, add anyway as last resort tournaments_to_display.append(tournament) + # Ensure we have exactly what we need (limit to 2) + tournaments_to_display = tournaments_to_display[:2] + + # Log the prioritization for debugging + print(f"Event {event_id} broadcast prioritization:") + print(f" - Ongoing tournaments: {len(ongoing_tournaments)}") + print(f" - Starting soon tournaments: {len(starting_soon_tournaments)}") + print(f" - Other tournaments with content: {len(other_tournaments_with_content)}") + print(f" - Final selection: {[str(t.id) for t in tournaments_to_display]}") + tournament_ids = [str(tournament.id) for tournament in tournaments_to_display] # Create QR code URL for the event broadcast