Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -702,7 +702,7 @@
"last_updated": "2026-04-09",
"verified": true,
"screenshot": "",
"latest_version": "2.3.0"
"latest_version": "2.4.0"
},
{
"id": "web-ui-info",
Expand Down
5 changes: 5 additions & 0 deletions plugins/masters-tournament/config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@
"maximum": 60,
"description": "Time to show each hole (seconds)"
},
"show_divider": {
"type": "boolean",
"default": true,
"description": "Show the vertical divider line between the hole info and map columns. Set to false for a cleaner single-cell look."
},
"featured_holes": {
"type": "array",
"items": {
Expand Down
17 changes: 13 additions & 4 deletions plugins/masters-tournament/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import logging
import os
import time
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from PIL import Image
Expand All @@ -21,6 +21,7 @@
from masters_renderer_enhanced import MastersRendererEnhanced
from logo_loader import MastersLogoLoader
from masters_helpers import (
_masters_thursday,
calculate_tournament_countdown,
filter_favorite_players,
get_detailed_phase,
Expand Down Expand Up @@ -451,7 +452,12 @@ def _display_course_tour(self, force_clear: bool) -> bool:
self._last_hole_advance["course_tour"] = now
elif last == 0:
self._last_hole_advance["course_tour"] = now
return self._show_image(self.renderer.render_hole_card(self._current_hole))
show_divider = self.config.get("display_modes", {}).get(
"course_tour", {}
).get("show_divider", True)
return self._show_image(
self.renderer.render_hole_card(self._current_hole, show_divider=show_divider)
)

def _display_amen_corner(self, force_clear: bool) -> bool:
return self._show_image(self.renderer.render_amen_corner())
Expand Down Expand Up @@ -527,8 +533,11 @@ def _display_countdown(self, force_clear: bool) -> bool:
target = meta["start_date"]
else:
# Hard fallback — should be unreachable, but keep the screen alive.
now = datetime.utcnow()
target = datetime(now.year, 4, 10, 12, 0, 0)
now = datetime.now(timezone.utc)
year = now.year
target = _masters_thursday(year)
if target <= now:
target = _masters_thursday(year + 1)
countdown = calculate_tournament_countdown(target)
return self._show_image(
self.renderer.render_countdown(
Expand Down
7 changes: 6 additions & 1 deletion plugins/masters-tournament/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"id": "masters-tournament",
"name": "Masters Tournament",
"version": "2.3.0",
"version": "2.4.0",
"description": "Broadcast-quality Masters Tournament display with real ESPN player headshots, accurate Augusta National hole layouts, fun facts, past champions, live leaderboards, and pixel-perfect LED matrix rendering",
"author": "ChuckBuilds",
"class_name": "MastersTournamentPlugin",
Expand Down Expand Up @@ -43,6 +43,11 @@
"height": 64
},
"versions": [
{
"version": "2.4.0",
"released": "2026-04-10",
"ledmatrix_min_version": "2.0.0"
},
{
"version": "2.3.0",
"released": "2026-04-10",
Expand Down
24 changes: 14 additions & 10 deletions plugins/masters-tournament/masters_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,16 +267,17 @@ def _format_tee_time_et(cls, iso_value: Optional[str]) -> str:
return f"{display_hour}:{minute:02d} {suffix}"

def _computed_fallback_meta(self) -> Dict:
"""Compute a best-guess Masters window: second Thursday of April.
"""Compute a best-guess Masters window using the correct Thursday rule.

Used only when ESPN doesn't currently return the Masters (off-season).
"""
from masters_helpers import _masters_thursday
now = datetime.now(timezone.utc)
year = now.year
start = self._second_thursday_of_april(year)
start = _masters_thursday(year)
if now > start + timedelta(days=4):
start = self._second_thursday_of_april(year + 1)
# Cover all four calendar days (ThuSun) through end-of-day, matching
start = _masters_thursday(year + 1)
# Cover all four calendar days (Thu-Sun) through end-of-day, matching
# the normalization applied to ESPN's parsed endDate.
end = start + timedelta(days=3, hours=23, minutes=59, seconds=59)
return {
Expand All @@ -290,12 +291,15 @@ def _computed_fallback_meta(self) -> Dict:

@staticmethod
def _second_thursday_of_april(year: int) -> datetime:
"""Second Thursday of April at 12:00 UTC (≈ 8am ET tee-off)."""
d = datetime(year, 4, 1, 12, 0, 0, tzinfo=timezone.utc)
# weekday(): Mon=0 … Thu=3
days_to_first_thursday = (3 - d.weekday()) % 7
first_thursday = d + timedelta(days=days_to_first_thursday)
return first_thursday + timedelta(days=7)
"""Backwards-compatible alias — delegates to the correct algorithm.

The original 'second Thursday of April' rule gave wrong dates for
2022 (Apr 13, actual Apr 7) and 2023 (Apr 13, actual Apr 6).
The correct rule is the Thursday between April 6-12 inclusive.
This method is kept so any external callers are not broken.
"""
from masters_helpers import _masters_thursday
return _masters_thursday(year)

# ── Schedule / tee times ─────────────────────────────────────

Expand Down
110 changes: 73 additions & 37 deletions plugins/masters-tournament/masters_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,31 @@ def calculate_scoring_average(rounds: List[Optional[int]]) -> Optional[float]:
return sum(valid_rounds) / len(valid_rounds)


def _masters_thursday(year: int) -> datetime:
"""Return the Masters Tournament start date (Thursday) for the given year.

The Masters always starts on the Thursday that falls between April 6 and
April 12 inclusive — the first full Mon-Sun week of April that contains
that Thursday. Verified against historical data:

2022 (Apr 1 = Fri) -> Apr 7
2023 (Apr 1 = Sat) -> Apr 6
2024 (Apr 1 = Mon) -> Apr 11
2025 (Apr 1 = Tue) -> Apr 10
2026 (Apr 1 = Wed) -> Apr 9
2027 (Apr 1 = Thu) -> Apr 8
2028 (Apr 1 = Sat) -> Apr 6

Returns a timezone-aware datetime at 12:00 UTC (approx. 8 am ET tee-off).
"""
for day in range(6, 13): # April 6 .. April 12 inclusive
d = datetime(year, 4, day, 12, 0, 0, tzinfo=timezone.utc)
if d.weekday() == 3: # Thursday = 3
return d
# Unreachable — there is always exactly one Thursday in any 7-day span.
raise RuntimeError(f"No Thursday found between April 6-12 for year {year}")


def _to_eastern(date: Optional[datetime]) -> datetime:
"""Normalize a datetime to Eastern (Augusta) time."""
if date is None:
Expand Down Expand Up @@ -373,12 +398,19 @@ def get_tournament_phase(
return "practice"
return "off-season"

if date.month == 4:
if 7 <= date.day <= 9:
return "practice"
elif 10 <= date.day <= 13:
return "tournament"

# Fallback: compute the correct Thursday dynamically.
thu = _masters_thursday(date.year)
thu_e = _to_eastern(thu)
thu_date = thu_e.date()
start_date_d = thu_date
end_date_d = thu_date + timedelta(days=3)
practice_start = thu_date - timedelta(days=3)
date_d = date.date()

if start_date_d <= date_d <= end_date_d:
return "tournament"
if practice_start <= date_d < start_date_d:
return "practice"
return "off-season"


Expand Down Expand Up @@ -429,39 +461,43 @@ def get_detailed_phase(
return "pre-tournament"
return "off-season"

month = date.month
day = date.day
# Fallback: compute the correct tournament window dynamically using the
# April 6-12 Thursday rule so this stays accurate in future years.
# Use calendar-day comparisons so early-morning hours on tournament days
# (e.g. 6 am Thursday) are not misclassified as "practice".
hour = date.hour
thu = _masters_thursday(date.year)
thu_e = _to_eastern(thu)
thu_date = thu_e.date()
sun_date = thu_date + timedelta(days=3) # Sunday = last day
mon_date = thu_date - timedelta(days=3) # Monday = first practice day
date_date = date.date()

# Tournament days: Thursday through Sunday (calendar day)
if thu_date <= date_date <= sun_date:
if hour < 6:
return "tournament-overnight"
if hour < 8:
return "tournament-morning"
if hour < 19:
return "tournament-live"
return "tournament-evening"

# Day after the tournament ends (Monday)
if date_date == sun_date + timedelta(days=1):
return "post-tournament"

# Practice rounds: Mon-Wed of Masters week
if mon_date <= date_date < thu_date:
return "practice"

# Pre-tournament: up to 2 weeks before (build anticipation).
# Compare date objects to avoid mixing aware/naive datetimes.
if timedelta(0) < (thu_date - date_date) <= timedelta(days=14):
return "pre-tournament"

# Masters is typically the second full week of April
# Practice: Mon-Wed, Tournament: Thu-Sun
# Adjust these dates each year as needed
if month == 4:
# Week before Masters (build anticipation)
if 1 <= day <= 6:
return "pre-tournament"

# Practice rounds
if 7 <= day <= 9:
return "practice"

# Tournament days (Thu=10 through Sun=13)
if 10 <= day <= 13:
if hour < 6:
return "tournament-overnight"
elif hour < 8:
return "tournament-morning"
elif hour < 19:
return "tournament-live"
else:
return "tournament-evening"

# Monday after
if day == 14:
return "post-tournament"

# March - countdown month
if month == 3 and day >= 20:
# Late March counts as pre-tournament too
if date.month == 3 and date.day >= 20:
return "pre-tournament"

return "off-season"
Expand Down
3 changes: 2 additions & 1 deletion plugins/masters-tournament/masters_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,8 @@ def _render_player_card_wide_short(self, img, draw, player, raw_name,

def render_hole_card(self, hole_number: int,
card_width: Optional[int] = None,
card_height: Optional[int] = None) -> Optional[Image.Image]:
card_height: Optional[int] = None,
show_divider: bool = True) -> Optional[Image.Image]:
cw = card_width if card_width is not None else self.width
ch = card_height if card_height is not None else self.height
hole_info = get_hole_info(hole_number)
Expand Down
27 changes: 16 additions & 11 deletions plugins/masters-tournament/masters_renderer_enhanced.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ def render_player_card(self, player: Dict,

def render_hole_card(self, hole_number: int,
card_width: Optional[int] = None,
card_height: Optional[int] = None) -> Optional[Image.Image]:
card_height: Optional[int] = None,
show_divider: bool = True) -> Optional[Image.Image]:
"""Enhanced hole card with two layout modes, chosen by vertical resolution:

* **ch >= 48** → existing "big" layout: a single text column on the
Expand Down Expand Up @@ -259,15 +260,16 @@ def render_hole_card(self, hole_number: int,

if ch >= self._HOLE_COMPACT_HEIGHT:
return self._render_hole_card_with_image(
img, draw, hole_number, hole_info, cw, ch,
img, draw, hole_number, hole_info, cw, ch, show_divider=show_divider,
)

return self._render_hole_card_compact(
img, draw, hole_number, hole_info, cw, ch,
img, draw, hole_number, hole_info, cw, ch, show_divider=show_divider,
)

def _render_hole_card_with_image(self, img, draw, hole_number: int,
hole_info: Dict, cw: int, ch: int) -> Image.Image:
hole_info: Dict, cw: int, ch: int,
show_divider: bool = True) -> Image.Image:
"""Large-canvas layout: single text column on the left + hole image on the right.

Text column contents (stacked top to bottom):
Expand All @@ -284,9 +286,10 @@ def _render_hole_card_with_image(self, img, draw, hole_number: int,

show_image = left_w < cw

# Left panel background strip
draw.rectangle([(0, 0), (left_w - 1, ch - 1)], fill=COLORS["masters_dark"])
if show_image:
# Left panel background strip (suppressed when show_divider=False for a unified green look)
if show_divider:
draw.rectangle([(0, 0), (left_w - 1, ch - 1)], fill=COLORS["masters_dark"])
if show_image and show_divider:
draw.line([(left_w - 1, 0), (left_w - 1, ch)], fill=COLORS["masters_yellow"])

line_h = self._text_height(draw, "A", self.font_detail) + 1
Expand Down Expand Up @@ -387,7 +390,8 @@ def _render_hole_card_with_image(self, img, draw, hole_number: int,
def _render_hole_card_compact(self, img, draw, hole_number: int,
hole_info: Dict,
cw: Optional[int] = None,
ch: Optional[int] = None) -> Image.Image:
ch: Optional[int] = None,
show_divider: bool = True) -> Image.Image:
"""Compact hole card for vertical resolutions below 48px.

All text stacked on the left, course image on the right.
Expand Down Expand Up @@ -455,9 +459,10 @@ def _render_hole_card_compact(self, img, draw, hole_number: int,
fill=COLORS["masters_yellow"], font=text_font)

if show_image:
# Divider between text and image
draw.line([(text_w, 0), (text_w, ch - 1)],
fill=COLORS["masters_yellow"])
# Divider between text and image (suppressed when show_divider=False)
if show_divider:
draw.line([(text_w, 0), (text_w, ch - 1)],
fill=COLORS["masters_yellow"])

# Course image on the right
hole_img = self.logo_loader.get_hole_image(
Expand Down