From 586de4431fc70d120d8fc6f056436aeac1c9ea4d Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Sep 2025 15:44:20 +0200 Subject: [PATCH 1/6] Remove Broadcasted Auto Event Template --- .../broadcast/broadcasted_auto_event.html | 263 ------------------ tournaments/views.py | 79 +++++- 2 files changed, 72 insertions(+), 270 deletions(-) delete mode 100644 tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html b/tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html deleted file mode 100644 index cf8a9f7..0000000 --- a/tournaments/templates/tournaments/broadcast/broadcasted_auto_event.html +++ /dev/null @@ -1,263 +0,0 @@ - - - {% load static %} - {% load qr_code %} - - - {% include 'tournaments/broadcast/base_head.html' %} - - - - Broadcast - - - - - - - - -
-
- -
- - -
-
-
- - - - - - - - -
- -
-
- -
- - diff --git a/tournaments/views.py b/tournaments/views.py index d9f7475..3e392e0 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -532,11 +532,29 @@ def automatic_broadcast(request, tournament_id): def automatic_broadcast_event(request, event_id): event = get_object_or_404(Event, pk=event_id) - tournaments = Tournament.objects.filter(event=event).order_by('start_date') + now = timezone.now() - # Filter tournaments that have broadcast content - tournaments_with_content = [] - for tournament in tournaments: + # Initial time windows + past_cutoff = now - timedelta(hours=4) + future_cutoff = now + timedelta(hours=4) + + # Get recent/current tournaments (started but not too old) + recent_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: try: content = tournament.broadcast_content() has_content = ( @@ -546,12 +564,59 @@ def automatic_broadcast_event(request, event_id): len(content.get('rankings', [])) > 0 ) if has_content: - tournaments_with_content.append(tournament) + recent_with_content.append(tournament) except: - # Skip tournaments that error when getting content continue - tournament_ids = [str(tournament.id) for tournament in tournaments_with_content] + tournaments_to_display = recent_with_content[:] + + # If we have 0 or 1 recent tournaments, extend the search + if len(recent_with_content) <= 1: + needed = 2 - len(recent_with_content) + + # 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 + + # 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') + + still_needed = 2 - len(tournaments_to_display) + for tournament in extended_future_tournaments[:still_needed]: + tournaments_to_display.append(tournament) + + tournament_ids = [str(tournament.id) for tournament in tournaments_to_display] # Create QR code URL for the event broadcast qr_code_url_val = reverse('automatic-broadcast-event', args=[event_id]) From aa7c9bc5aa8caab53cc013adfc1b72b1e00444e5 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Sep 2025 16:23:32 +0200 Subject: [PATCH 2/6] Update auto-broadcast completion logic for tournaments Refine tournament auto-broadcast detection algorithm to handle multi-page tournaments more robustly. Specifically: - Improve completion detection for multi-page tournaments - Prevent false completion when cycling through pages - Add more descriptive logging for edge cases --- .../tournaments/broadcast/broadcasted_auto.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_auto.html b/tournaments/templates/tournaments/broadcast/broadcasted_auto.html index dff76ea..f8b85a9 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_auto.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_auto.html @@ -266,8 +266,9 @@ const pageCount = data.pageCount(); const elapsed = Date.now() - startTime; + // Skip tournaments with no pages (this shouldn't happen now, but just in case) if (pageCount === 0) { - console.log('No pages to display yet'); + console.log('Tournament has no content, should have been filtered out'); return; } @@ -289,10 +290,12 @@ reason = 'Single page - 15 seconds elapsed'; } } else if (pageCount > 1) { - // Multi-page tournament: complete when we cycle back to page 1 after seeing all pages - if (currentActive === 1 && lastActive > 1 && maxPagesSeen >= pageCount) { + // Multi-page tournament: complete when we're about to go back to page 1 after seeing all pages + // Check if we're at the last page and have seen all pages + if (currentActive === pageCount && maxPagesSeen >= pageCount && lastActive !== null) { + // We're at the last page and have seen all pages - complete now before going back to page 1 shouldComplete = true; - reason = 'Multi-page - full cycle completed'; + reason = 'Multi-page - completed at last page (avoiding first page repeat)'; } else if (elapsed >= completionTime) { // Fallback: complete after (pageCount * 15 seconds) shouldComplete = true; From c0d18ed9a14e42326886859ec7da9f7911a40da7 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Sep 2025 16:28:24 +0200 Subject: [PATCH 3/6] Add retry mechanism for tournament autobroadcast --- .../broadcast/broadcasted_event_auto.html | 84 +++++++++++++++++-- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_event_auto.html b/tournaments/templates/tournaments/broadcast/broadcasted_event_auto.html index b328665..308e6c7 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_event_auto.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_event_auto.html @@ -22,46 +22,112 @@ display: block; background-color: #000; } + + .error-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 18px; + text-align: center; + z-index: 1000; + display: none; + } +
+ Rechargement du tournoi... +
From 61a6f88e6f2eb2da3a0879b165eb126e50b78451 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Fri, 5 Sep 2025 16:30:57 +0200 Subject: [PATCH 4/6] Update broadcasted_auto.html --- .../tournaments/broadcast/broadcasted_auto.html | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tournaments/templates/tournaments/broadcast/broadcasted_auto.html b/tournaments/templates/tournaments/broadcast/broadcasted_auto.html index f8b85a9..dff76ea 100644 --- a/tournaments/templates/tournaments/broadcast/broadcasted_auto.html +++ b/tournaments/templates/tournaments/broadcast/broadcasted_auto.html @@ -266,9 +266,8 @@ const pageCount = data.pageCount(); const elapsed = Date.now() - startTime; - // Skip tournaments with no pages (this shouldn't happen now, but just in case) if (pageCount === 0) { - console.log('Tournament has no content, should have been filtered out'); + console.log('No pages to display yet'); return; } @@ -290,12 +289,10 @@ reason = 'Single page - 15 seconds elapsed'; } } else if (pageCount > 1) { - // Multi-page tournament: complete when we're about to go back to page 1 after seeing all pages - // Check if we're at the last page and have seen all pages - if (currentActive === pageCount && maxPagesSeen >= pageCount && lastActive !== null) { - // We're at the last page and have seen all pages - complete now before going back to page 1 + // Multi-page tournament: complete when we cycle back to page 1 after seeing all pages + if (currentActive === 1 && lastActive > 1 && maxPagesSeen >= pageCount) { shouldComplete = true; - reason = 'Multi-page - completed at last page (avoiding first page repeat)'; + reason = 'Multi-page - full cycle completed'; } else if (elapsed >= completionTime) { // Fallback: complete after (pageCount * 15 seconds) shouldComplete = true; From 183d0ee6ec1c5be749d9ce972878fb09fc301cef Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 6 Sep 2025 09:47:52 +0200 Subject: [PATCH 5/6] Improve tournament broadcast selection logic --- tournaments/models/match.py | 31 +++++++ tournaments/models/tournament.py | 66 ++++++++++++++ tournaments/views.py | 147 ++++++++++++++++++------------- 3 files changed, 185 insertions(+), 59 deletions(-) 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 From 108d2d64512c63d17157dbe359f10f4160906be4 Mon Sep 17 00:00:00 2001 From: Razmig Sarkissian Date: Sat, 6 Sep 2025 09:50:40 +0200 Subject: [PATCH 6/6] Extend tournament broadcast search window to 72 hours --- tournaments/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tournaments/views.py b/tournaments/views.py index 4dfcdea..c0b58fe 100644 --- a/tournaments/views.py +++ b/tournaments/views.py @@ -535,8 +535,8 @@ def automatic_broadcast_event(request, event_id): now = timezone.now() # 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 + past_cutoff = now - timedelta(hours=72) # Extended past search + future_cutoff = now + timedelta(hours=72) # Extended future search all_tournaments = Tournament.objects.filter( event=event,