diff --git a/config/config.json b/config/config.json index f024fbd3..f7bb815b 100644 --- a/config/config.json +++ b/config/config.json @@ -170,7 +170,8 @@ }, "ncaa_fb": { "enabled": true, - "top_teams": 25 + "top_teams": 25, + "show_ranking": true }, "nhl": { "enabled": false, @@ -299,6 +300,7 @@ ], "logo_dir": "assets/sports/ncaa_fbs_logos", "show_records": true, + "show_ranking": false, "display_modes": { "ncaa_fb_live": true, "ncaa_fb_recent": true , diff --git a/src/leaderboard_manager.py b/src/leaderboard_manager.py index 5b21de4e..081ec44c 100644 --- a/src/leaderboard_manager.py +++ b/src/leaderboard_manager.py @@ -100,7 +100,8 @@ class LeaderboardManager: 'league_logo': 'assets/sports/ncaa_fbs_logos/ncaa_fb.png', 'teams_url': 'https://site.api.espn.com/apis/site/v2/sports/football/college-football/teams', 'enabled': self.enabled_sports.get('ncaa_fb', {}).get('enabled', False), - 'top_teams': self.enabled_sports.get('ncaa_fb', {}).get('top_teams', 25) + 'top_teams': self.enabled_sports.get('ncaa_fb', {}).get('top_teams', 25), + 'show_ranking': self.enabled_sports.get('ncaa_fb', {}).get('show_ranking', True) }, 'nhl': { 'sport': 'hockey', @@ -568,12 +569,29 @@ class LeaderboardManager: # Draw team standings horizontally in a single line team_x = current_x - # Use the same dynamic logo size calculated earlier - logo_size = int(height * 0.95) + # Use the same dynamic logo size as Odds Manager ticker + logo_size = int(height * 1.2) for i, team in enumerate(teams): - # Draw bold team number (centered vertically) - number_text = f"{i+1}." + # Draw bold team number/ranking/record (centered vertically) + if league_key == 'ncaa_fb': + if league_config.get('show_ranking', True): + # Show ranking number if available + if 'rank' in team and team['rank'] > 0: + number_text = f"#{team['rank']}" + else: + # Team is unranked - show position number as fallback + number_text = f"{i+1}." + else: + # Show record instead of ranking + if 'record_summary' in team: + number_text = team['record_summary'] + else: + number_text = f"{i+1}." + else: + # For other leagues, show position + number_text = f"{i+1}." + number_bbox = self.fonts['large'].getbbox(number_text) number_width = number_bbox[2] - number_bbox[0] number_height = number_bbox[3] - number_bbox[1] @@ -593,24 +611,24 @@ class LeaderboardManager: # Draw team abbreviation after the logo (centered vertically) team_text = team['abbreviation'] - text_bbox = self.fonts['large'].getbbox(team_text) + text_bbox = self.fonts['medium'].getbbox(team_text) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] text_x = logo_x + logo_size + 4 text_y = (height - text_height) // 2 - draw.text((text_x, text_y), team_text, font=self.fonts['large'], fill=(255, 255, 255)) + draw.text((text_x, text_y), team_text, font=self.fonts['medium'], fill=(255, 255, 255)) # Calculate total width used by this team team_width = number_width + 4 + logo_size + 4 + text_width + 12 # 12px spacing to next team else: # Fallback if no logo - draw team abbreviation after bold number (centered vertically) team_text = team['abbreviation'] - text_bbox = self.fonts['large'].getbbox(team_text) + text_bbox = self.fonts['medium'].getbbox(team_text) text_width = text_bbox[2] - text_bbox[0] text_height = text_bbox[3] - text_bbox[1] text_x = team_x + number_width + 4 text_y = (height - text_height) // 2 - draw.text((text_x, text_y), team_text, font=self.fonts['large'], fill=(255, 255, 255)) + draw.text((text_x, text_y), team_text, font=self.fonts['medium'], fill=(255, 255, 255)) # Calculate total width used by this team team_width = number_width + 4 + text_width + 12 # 12px spacing to next team diff --git a/src/ncaa_fb_managers.py b/src/ncaa_fb_managers.py index 8642e6bf..72a7999f 100644 --- a/src/ncaa_fb_managers.py +++ b/src/ncaa_fb_managers.py @@ -53,6 +53,7 @@ class BaseNCAAFBManager: # Renamed class self.logo_dir = self.ncaa_fb_config.get("logo_dir", "assets/sports/ncaa_fbs_logos") # Changed logo dir self.update_interval = self.ncaa_fb_config.get("update_interval_seconds", 60) self.show_records = self.ncaa_fb_config.get('show_records', False) + self.show_ranking = self.ncaa_fb_config.get('show_ranking', False) self.season_cache_duration = self.ncaa_fb_config.get("season_cache_duration_seconds", 86400) # 24 hours default # Number of games to show (instead of time-based windows) self.recent_games_to_show = self.ncaa_fb_config.get("recent_games_to_show", 5) # Show last 5 games @@ -95,10 +96,57 @@ class BaseNCAAFBManager: # Renamed class self.display_height = self.display_manager.matrix.height self._logo_cache = {} + + # Initialize team rankings cache + self._team_rankings_cache = {} + self._rankings_cache_timestamp = 0 + self._rankings_cache_duration = 3600 # Cache rankings for 1 hour self.logger.info(f"Initialized NCAAFB manager with display dimensions: {self.display_width}x{self.display_height}") self.logger.info(f"Logo directory: {self.logo_dir}") self.logger.info(f"Display modes - Recent: {self.recent_enabled}, Upcoming: {self.upcoming_enabled}, Live: {self.live_enabled}") + + def _fetch_team_rankings(self) -> Dict[str, int]: + """Fetch current team rankings from ESPN API.""" + current_time = time.time() + + # Check if we have cached rankings that are still valid + if (self._team_rankings_cache and + current_time - self._rankings_cache_timestamp < self._rankings_cache_duration): + return self._team_rankings_cache + + try: + rankings_url = "https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings" + response = self.session.get(rankings_url, headers=self.headers, timeout=30) + response.raise_for_status() + data = response.json() + + rankings = {} + rankings_data = data.get('rankings', []) + + if rankings_data: + # Use the first ranking (usually AP Top 25) + first_ranking = rankings_data[0] + teams = first_ranking.get('ranks', []) + + for team_data in teams: + team_info = team_data.get('team', {}) + team_abbr = team_info.get('abbreviation', '') + current_rank = team_data.get('current', 0) + + if team_abbr and current_rank > 0: + rankings[team_abbr] = current_rank + + # Cache the results + self._team_rankings_cache = rankings + self._rankings_cache_timestamp = current_time + + self.logger.debug(f"Fetched rankings for {len(rankings)} teams") + return rankings + + except Exception as e: + self.logger.error(f"Error fetching team rankings: {e}") + return {} def _get_timezone(self): try: @@ -1007,29 +1055,72 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class if 'odds' in game and game['odds']: self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height) - # Draw records if enabled - if self.show_records: + # Draw records or rankings if enabled + if self.show_records or self.show_ranking: try: record_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) except IOError: record_font = ImageFont.load_default() - away_record = game.get('away_record', '') - home_record = game.get('home_record', '') + # Get team abbreviations + away_abbr = game.get('away_abbr', '') + home_abbr = game.get('home_abbr', '') record_bbox = draw_overlay.textbbox((0,0), "0-0", font=record_font) record_height = record_bbox[3] - record_bbox[1] record_y = self.display_height - record_height - if away_record: - away_record_x = 0 - self._draw_text_with_outline(draw_overlay, away_record, (away_record_x, record_y), record_font) + # Display away team info + if away_abbr: + if self.show_ranking: + # Show ranking if available + rankings = self._fetch_team_rankings() + away_rank = rankings.get(away_abbr, 0) + if away_rank > 0: + away_text = f"#{away_rank}" + elif self.show_records: + # Only show record if show_records is enabled + away_text = game.get('away_record', '') + else: + # Show nothing if show_records is false and team is unranked + away_text = '' + else: + # Show record only if show_records is enabled + if self.show_records: + away_text = game.get('away_record', '') + else: + away_text = '' + + if away_text: + away_record_x = 0 + self._draw_text_with_outline(draw_overlay, away_text, (away_record_x, record_y), record_font) - if home_record: - home_record_bbox = draw_overlay.textbbox((0,0), home_record, font=record_font) - home_record_width = home_record_bbox[2] - home_record_bbox[0] - home_record_x = self.display_width - home_record_width - self._draw_text_with_outline(draw_overlay, home_record, (home_record_x, record_y), record_font) + # Display home team info + if home_abbr: + if self.show_ranking: + # Show ranking if available + rankings = self._fetch_team_rankings() + home_rank = rankings.get(home_abbr, 0) + if home_rank > 0: + home_text = f"#{home_rank}" + elif self.show_records: + # Only show record if show_records is enabled + home_text = game.get('home_record', '') + else: + # Show nothing if show_records is false and team is unranked + home_text = '' + else: + # Show record only if show_records is enabled + if self.show_records: + home_text = game.get('home_record', '') + else: + home_text = '' + + if home_text: + home_record_bbox = draw_overlay.textbbox((0,0), home_text, font=record_font) + home_record_width = home_record_bbox[2] - home_record_bbox[0] + home_record_x = self.display_width - home_record_width + self._draw_text_with_outline(draw_overlay, home_text, (home_record_x, record_y), record_font) # Composite and display main_img = Image.alpha_composite(main_img, overlay) @@ -1245,29 +1336,72 @@ class NCAAFBUpcomingManager(BaseNCAAFBManager): # Renamed class if 'odds' in game and game['odds']: self._draw_dynamic_odds(draw_overlay, game['odds'], self.display_width, self.display_height) - # Draw records if enabled - if self.show_records: + # Draw records or rankings if enabled + if self.show_records or self.show_ranking: try: record_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 6) except IOError: record_font = ImageFont.load_default() - away_record = game.get('away_record', '') - home_record = game.get('home_record', '') + # Get team abbreviations + away_abbr = game.get('away_abbr', '') + home_abbr = game.get('home_abbr', '') record_bbox = draw_overlay.textbbox((0,0), "0-0", font=record_font) record_height = record_bbox[3] - record_bbox[1] record_y = self.display_height - record_height - if away_record: - away_record_x = 0 - self._draw_text_with_outline(draw_overlay, away_record, (away_record_x, record_y), record_font) + # Display away team info + if away_abbr: + if self.show_ranking: + # Show ranking if available + rankings = self._fetch_team_rankings() + away_rank = rankings.get(away_abbr, 0) + if away_rank > 0: + away_text = f"#{away_rank}" + elif self.show_records: + # Only show record if show_records is enabled + away_text = game.get('away_record', '') + else: + # Show nothing if show_records is false and team is unranked + away_text = '' + else: + # Show record only if show_records is enabled + if self.show_records: + away_text = game.get('away_record', '') + else: + away_text = '' + + if away_text: + away_record_x = 0 + self._draw_text_with_outline(draw_overlay, away_text, (away_record_x, record_y), record_font) - if home_record: - home_record_bbox = draw_overlay.textbbox((0,0), home_record, font=record_font) - home_record_width = home_record_bbox[2] - home_record_bbox[0] - home_record_x = self.display_width - home_record_width - self._draw_text_with_outline(draw_overlay, home_record, (home_record_x, record_y), record_font) + # Display home team info + if home_abbr: + if self.show_ranking: + # Show ranking if available + rankings = self._fetch_team_rankings() + home_rank = rankings.get(home_abbr, 0) + if home_rank > 0: + home_text = f"#{home_rank}" + elif self.show_records: + # Only show record if show_records is enabled + home_text = game.get('home_record', '') + else: + # Show nothing if show_records is false and team is unranked + home_text = '' + else: + # Show record only if show_records is enabled + if self.show_records: + home_text = game.get('home_record', '') + else: + home_text = '' + + if home_text: + home_record_bbox = draw_overlay.textbbox((0,0), home_text, font=record_font) + home_record_width = home_record_bbox[2] - home_record_bbox[0] + home_record_x = self.display_width - home_record_width + self._draw_text_with_outline(draw_overlay, home_text, (home_record_x, record_y), record_font) # Composite and display main_img = Image.alpha_composite(main_img, overlay) diff --git a/test/test_ranking_toggle.py b/test/test_ranking_toggle.py new file mode 100644 index 00000000..703e32e7 --- /dev/null +++ b/test/test_ranking_toggle.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python3 +""" +Test script to demonstrate the new ranking/record toggle functionality +for both the leaderboard manager and NCAA FB managers. +""" + +import sys +import os +import json +import time +from typing import Dict, Any + +# Add the src directory to the path so we can import our modules +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src')) + +from leaderboard_manager import LeaderboardManager +from ncaa_fb_managers import BaseNCAAFBManager +from cache_manager import CacheManager +from config_manager import ConfigManager + +def test_leaderboard_ranking_toggle(): + """Test the leaderboard manager ranking toggle functionality.""" + + print("Testing Leaderboard Manager Ranking Toggle") + print("=" * 50) + + # Create a mock display manager + class MockDisplayManager: + def __init__(self): + self.matrix = type('Matrix', (), {'width': 64, 'height': 32})() + self.image = None + self.draw = None + + def update_display(self): + pass + + def set_scrolling_state(self, scrolling): + pass + + def process_deferred_updates(self): + pass + + # Test configuration with show_ranking enabled + config_ranking_enabled = { + 'leaderboard': { + 'enabled': True, + 'enabled_sports': { + 'ncaa_fb': { + 'enabled': True, + 'top_teams': 10, + 'show_ranking': True # Show rankings + } + }, + 'update_interval': 3600, + 'scroll_speed': 2, + 'scroll_delay': 0.05, + 'display_duration': 60, + 'loop': True, + 'request_timeout': 30, + 'dynamic_duration': True, + 'min_duration': 30, + 'max_duration': 300, + 'duration_buffer': 0.1, + 'time_per_team': 2.0, + 'time_per_league': 3.0 + } + } + + # Test configuration with show_ranking disabled + config_ranking_disabled = { + 'leaderboard': { + 'enabled': True, + 'enabled_sports': { + 'ncaa_fb': { + 'enabled': True, + 'top_teams': 10, + 'show_ranking': False # Show records + } + }, + 'update_interval': 3600, + 'scroll_speed': 2, + 'scroll_delay': 0.05, + 'display_duration': 60, + 'loop': True, + 'request_timeout': 30, + 'dynamic_duration': True, + 'min_duration': 30, + 'max_duration': 300, + 'duration_buffer': 0.1, + 'time_per_team': 2.0, + 'time_per_league': 3.0 + } + } + + try: + display_manager = MockDisplayManager() + + # Test with ranking enabled + print("1. Testing with show_ranking = True") + leaderboard_manager = LeaderboardManager(config_ranking_enabled, display_manager) + ncaa_fb_config = leaderboard_manager.league_configs['ncaa_fb'] + print(f" show_ranking config: {ncaa_fb_config.get('show_ranking', 'Not set')}") + + standings = leaderboard_manager._fetch_standings(ncaa_fb_config) + if standings: + print(f" Fetched {len(standings)} teams") + print(" Top 5 teams with rankings:") + for i, team in enumerate(standings[:5]): + rank = team.get('rank', 'N/A') + record = team.get('record_summary', 'N/A') + print(f" {i+1}. {team['name']} ({team['abbreviation']}) - Rank: #{rank}, Record: {record}") + + print("\n2. Testing with show_ranking = False") + leaderboard_manager = LeaderboardManager(config_ranking_disabled, display_manager) + ncaa_fb_config = leaderboard_manager.league_configs['ncaa_fb'] + print(f" show_ranking config: {ncaa_fb_config.get('show_ranking', 'Not set')}") + + standings = leaderboard_manager._fetch_standings(ncaa_fb_config) + if standings: + print(f" Fetched {len(standings)} teams") + print(" Top 5 teams with records:") + for i, team in enumerate(standings[:5]): + rank = team.get('rank', 'N/A') + record = team.get('record_summary', 'N/A') + print(f" {i+1}. {team['name']} ({team['abbreviation']}) - Rank: #{rank}, Record: {record}") + + print("\nāœ“ Leaderboard ranking toggle test completed!") + return True + + except Exception as e: + print(f"āœ— Error testing leaderboard ranking toggle: {e}") + import traceback + traceback.print_exc() + return False + +def test_ncaa_fb_ranking_toggle(): + """Test the NCAA FB manager ranking toggle functionality.""" + + print("\nTesting NCAA FB Manager Ranking Toggle") + print("=" * 50) + + # Create a mock display manager + class MockDisplayManager: + def __init__(self): + self.matrix = type('Matrix', (), {'width': 64, 'height': 32})() + self.image = None + self.draw = None + + def update_display(self): + pass + + def set_scrolling_state(self, scrolling): + pass + + def process_deferred_updates(self): + pass + + # Test configurations + configs = [ + { + 'name': 'show_ranking=true, show_records=true', + 'config': { + 'ncaa_fb_scoreboard': { + 'enabled': True, + 'show_records': True, + 'show_ranking': True, + 'logo_dir': 'assets/sports/ncaa_fbs_logos', + 'display_modes': { + 'ncaa_fb_live': True, + 'ncaa_fb_recent': True, + 'ncaa_fb_upcoming': True + } + } + } + }, + { + 'name': 'show_ranking=true, show_records=false', + 'config': { + 'ncaa_fb_scoreboard': { + 'enabled': True, + 'show_records': False, + 'show_ranking': True, + 'logo_dir': 'assets/sports/ncaa_fbs_logos', + 'display_modes': { + 'ncaa_fb_live': True, + 'ncaa_fb_recent': True, + 'ncaa_fb_upcoming': True + } + } + } + }, + { + 'name': 'show_ranking=false, show_records=true', + 'config': { + 'ncaa_fb_scoreboard': { + 'enabled': True, + 'show_records': True, + 'show_ranking': False, + 'logo_dir': 'assets/sports/ncaa_fbs_logos', + 'display_modes': { + 'ncaa_fb_live': True, + 'ncaa_fb_recent': True, + 'ncaa_fb_upcoming': True + } + } + } + }, + { + 'name': 'show_ranking=false, show_records=false', + 'config': { + 'ncaa_fb_scoreboard': { + 'enabled': True, + 'show_records': False, + 'show_ranking': False, + 'logo_dir': 'assets/sports/ncaa_fbs_logos', + 'display_modes': { + 'ncaa_fb_live': True, + 'ncaa_fb_recent': True, + 'ncaa_fb_upcoming': True + } + } + } + } + ] + + try: + display_manager = MockDisplayManager() + cache_manager = CacheManager() + + for i, test_config in enumerate(configs, 1): + print(f"{i}. Testing: {test_config['name']}") + ncaa_fb_manager = BaseNCAAFBManager(test_config['config'], display_manager, cache_manager) + print(f" show_records: {ncaa_fb_manager.show_records}") + print(f" show_ranking: {ncaa_fb_manager.show_ranking}") + + # Test fetching rankings + rankings = ncaa_fb_manager._fetch_team_rankings() + if rankings: + print(f" Fetched rankings for {len(rankings)} teams") + print(" Sample rankings:") + for j, (team_abbr, rank) in enumerate(list(rankings.items())[:3]): + print(f" {team_abbr}: #{rank}") + print() + + print("āœ“ NCAA FB ranking toggle test completed!") + print("\nLogic Summary:") + print("- show_ranking=true, show_records=true: Shows #5 if ranked, 2-0 if unranked") + print("- show_ranking=true, show_records=false: Shows #5 if ranked, nothing if unranked") + print("- show_ranking=false, show_records=true: Shows 2-0 (record)") + print("- show_ranking=false, show_records=false: Shows nothing") + return True + + except Exception as e: + print(f"āœ— Error testing NCAA FB ranking toggle: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """Main function to run all tests.""" + print("NCAA Football Ranking/Record Toggle Test") + print("=" * 60) + print("This test demonstrates the new functionality:") + print("- Leaderboard manager can show poll rankings (#5) or records (2-0)") + print("- NCAA FB managers can show poll rankings (#5) or records (2-0)") + print("- Configuration controls which is displayed") + print() + + try: + success1 = test_leaderboard_ranking_toggle() + success2 = test_ncaa_fb_ranking_toggle() + + if success1 and success2: + print("\nšŸŽ‰ All tests passed! The ranking/record toggle is working correctly.") + print("\nConfiguration Summary:") + print("- Set 'show_ranking': true in config to show poll rankings (#5)") + print("- Set 'show_ranking': false in config to show season records (2-0)") + print("- Works in both leaderboard and NCAA FB scoreboard managers") + else: + print("\nāŒ Some tests failed. Please check the errors above.") + except KeyboardInterrupt: + print("\nTest interrupted by user") + except Exception as e: + print(f"Error running tests: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + main()