From 72257c8ebe621804e54c5471f9ad134f2abd7d65 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Thu, 3 Sep 2026 23:23:38 -0400 Subject: [PATCH] docs(nrl-scoreboard): document all 131 settings, with real renders The README covered roughly 55 of the plugin's 131 settings and had no images at all. It now documents every schema leaf -- verified by a token audit against config_schema.json -- and shows each of the three display modes, the show_records toggle, and the card at four panel sizes. Three things the old README got wrong or left out: - The seven selection limits (recent_games_to_show and friends) are declared twice, at the config root and under game_limits, and both are read. game_limits wins where present. Same for show_records / show_ranking / show_odds under display_options, and for show_favorite_teams_only under filtering. Setting the root copy while the nested one exists looks like a setting that does nothing. - other_games_min_quality's documented choices included "broadcast", which the enum does not offer. - The scroll_settings block is unreachable (#422); the README now says so rather than describing six knobs that do nothing. mode_durations and customization.favorite_result_colors are read by the core, not by this plugin, so they look dead to a grep of this tree. The README now says where they are read. Renders come from docs/assets/nrl-scoreboard/shots.json. The fixture seeds games onto the per-mode sub-managers rather than the plugin -- every mode's display() returns early unless games_list is populated, and the live path also needs live_games -- and passes display_mode, without which the plugin's no-argument path selects nothing and draws a blank panel. The bundled PEN/MEL logo files are grey placeholders, so the crests render as blocks; the README says so. check_plugin.py passes on all eight panel sizes for all three modes. Carries the docs-tooling changes from #423 (display_mode shot key, dotted-path attrs, *_path coercion), which this render depends on. Co-Authored-By: Claude Opus 5 --- docs/assets/nrl-scoreboard/display-modes.png | Bin 0 -> 17216 bytes docs/assets/nrl-scoreboard/hero.png | Bin 0 -> 2839 bytes docs/assets/nrl-scoreboard/panel-sizes.png | Bin 0 -> 18082 bytes docs/assets/nrl-scoreboard/shots.json | 832 +++++++++++++++++++ docs/assets/nrl-scoreboard/show-records.png | Bin 0 -> 10465 bytes plugins.json | 2 +- plugins/nrl-scoreboard/README.md | 664 ++++++++++----- plugins/nrl-scoreboard/manifest.json | 12 +- scripts/docs_render_support/sitecustomize.py | 55 +- scripts/render_docs_assets.py | 3 + 10 files changed, 1363 insertions(+), 205 deletions(-) create mode 100644 docs/assets/nrl-scoreboard/display-modes.png create mode 100644 docs/assets/nrl-scoreboard/hero.png create mode 100644 docs/assets/nrl-scoreboard/panel-sizes.png create mode 100644 docs/assets/nrl-scoreboard/shots.json create mode 100644 docs/assets/nrl-scoreboard/show-records.png diff --git a/docs/assets/nrl-scoreboard/display-modes.png b/docs/assets/nrl-scoreboard/display-modes.png new file mode 100644 index 0000000000000000000000000000000000000000..ea443154c3e45eac35f5b65951111a57183cedb8 GIT binary patch literal 17216 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 ig#YTTV@hmogJWYFhB?7YC>Oh^m8NSB zGcT}kIhul3G)%;0YPyt`k{BX6MN$!ri;4)IpQqFFSf}SX=l$dNyzldSFW>j`{d_*} z_v$zP$W>OKSb;#GRR{L(Jq!XZ2fCnUOAFxZHJm#I0$B|n*t;k2{PRgE?8=Fw;FVME zx3&!DuW{ZGE!8$99hNjEi=lLZobh>z^^+S9uouksZ6iee|#Qn2z6br zww^PWW)T9W&bFS8i30*$W(o2ES%7R)L7){5pwu(fqOA`5o@@0o>ViIAmzI`xV*_8F z&}Qh&jV)4>#X=$OcxD{E$~=8J#I)-p6=)gA(gy^xumz=pR)8FU;pOeF5vS%ZXF7 z?cjhoee!|xhP;9QQ9YRvqsaxORZbj%kifAq`h6@)b{JUp8jU5udY{et?dqYiR^z{+ZM zcD3{DiM|RjZN0}iJ6daNEB%<*FN^>`_PdpKqV^HCL|&d9o%~1TKKCKZh;GMk6C{du z9x_4V+s+f#Z}1k(po&G`t$d3Oi&=6QJ-aTL5^Yio7PdPV78b^7H-<^Go6h!enAI_o zIwszrkuxEEPP55~tbTCL zzj*gr?xr&2CgRVmB3KfKg~Q?OXwv&hVA`al9!yivQ#h>h@^U*Gv}u;fnxAUO^CEYNJ4kumWvpWJ3vH#DFF{Tc9$-0YeRd&>D{ zvRNxcK%HeIt)Kqp%UA98P-p7=E2N&UI9HwLdHOw~c{q@CL5`i(Zn-&t{qg>_bzpGG zv-NsiPXL%U+@6DEhYq!ZFime2; z-0GYpO$b5}4f31qbwUMQ-iQt3Cv#a|Q_ago?wEVu{Uh2GWk{Bv9S0KfM(F?1z0c!# zaPZD}S9$jB0j++rUbc=_FQdSsO<(iyBad#dynU(d8z0Md%X&sM&L~(~lbfXvWgTsdSGlT27s?WGh{*cmcRDLC3b1s&>XSlNM!ZA&4NosPa8l&hgc5$|+%!Z?ge_nOE)_MD@jO2#W=J}U( z$l@>Jm@F^dldpW`BOSRSGJN4;d|`&W5^m2=2&)f7Q<5C1o%w(_`piOI-@@ukaIhdz zH4qX=PaeO^^7HCg6HJ*K`-Q178`VfQRN0SZusKu8{u*DoxGK^&YDOg$tv$tqIS6|4 zu<9hG7!_!q*9w`MXQ8ocX+an=m!*`{SFpB0p~kRfn;EO~oIV`w1ls;m3|GlCO=^i< z5VuzJ$fnw^uCC}$F_|O^x->EU9p7FAZ3hP6aLPoNhb#*X^`V@CBia{jh~=GPUp&{V zInEx)pfQB8V_A>L(Nf3C5iMct=^dt~8>Y6Sl^}E0(Tr+=Y{shX6q(Ftm9yD?vjw4} z%SZW~sEgsMlhI?oofBt5!l_0Qe6B)ZMhA8?5VG(tqPKBI4RsFU9KTC%A(dN$x7*PK z=DD(#Lsjo{OUEkfZHNPu$>&@qUOz4Z*w0X~M<|>D`Hwk+PodxE(1Y$R+C}yT&!<|B0;TmKP*zJ$qoa8-HQ-I+~9v9@#h84ctiC{?*)QRON{qUg@bHl+o zSfuosmm#Q90%r)@Vg;sSf$-TEmza1(aZ=(J#|BH%rHYz0PX2Z@2Gz_a`ys}kku^ub*rvjEh`=ttsD^g&KDh4LkxSy$Mo|7oF-FY*4^H}sWs~jjCfXAii zdK`!NiIFM=Ue*WH3vX2nwx=$%T4Ep8#^2TT)jy3%+tJps7b2WexaW*-sMGy)U7Nhh zI5w$vZXeJQR>2r$0v9p65GM}k9Z_o`O%OZ3^T>dS6K<`WCAA!bR z_)RyyYa{OI9L-Tcjn-ge^&We0ffbRAj5j%4vc|HeIW@o86H`)fxDQ``pwQ@c<^{hAr%pfe zHB}m_?K!&BpncE~5*n&aOM71|Cr-L1IkJ1&SFng*tml0GZrQN7DUi6*HXo0Pi3xv; va1Ho^p8wF}t#_?^*Oj$g_?hhe4e#{4&-4BI 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 Ca?n2j literal 0 HcmV?d00001 diff --git a/plugins.json b/plugins.json index f818927f..ec3cafd0 100644 --- a/plugins.json +++ b/plugins.json @@ -1095,7 +1095,7 @@ "last_updated": "2026-09-02", "verified": true, "screenshot": "", - "latest_version": "1.21.1" + "latest_version": "1.21.2" }, { "id": "jellyfin-now-playing", diff --git a/plugins/nrl-scoreboard/README.md b/plugins/nrl-scoreboard/README.md index 33ff3e77..b048b8ef 100644 --- a/plugins/nrl-scoreboard/README.md +++ b/plugins/nrl-scoreboard/README.md @@ -1,85 +1,48 @@ -# NRL Scoreboard Plugin +# NRL Scoreboard Live, recent, and upcoming **NRL (National Rugby League)** games on your -LEDMatrix display, sourced from ESPN's public rugby-league API (no API key -required). +LEDMatrix display, from ESPN's public rugby-league API. No API key, no account, +no configuration beyond picking your clubs. -This plugin is a single-league fork of the `soccer-scoreboard` plugin and keeps -full feature parity: switch or scroll display, live-game priority, goal/win -celebrations, dynamic per-mode durations, and Vegas continuous-scroll support. - -## Display Modes - -The plugin exposes three display modes you can enable independently: - -- `nrl_live` — games currently in progress (running clock, 1H/2H/HALF/ET) -- `nrl_recent` — recently completed games with final scores -- `nrl_upcoming` — scheduled games with date/time - -Each mode can be shown as **switch** (one game at a time, timed) or **scroll** -(all games scroll horizontally at high FPS). - -## Data Source & the `3` league-slug quirk +![NRL live scorebug](../../docs/assets/nrl-scoreboard/hero.png) -Games come from: - -``` -https://site.api.espn.com/apis/site/v2/sports/rugby-league/3/scoreboard -``` - -Note the `3` in the path. That is **ESPN's internal numeric slug for the NRL** -under the `rugby-league` sport — it is *not* a typo and must **not** be changed -to `nrl`. The human-facing web path is `/nrl/`, but the API path segment is the -literal string `3` (confirmed via -`site.web.api.espn.com/apis/v2/scoreboard/header?sport=rugby-league`, which lists -NRL as `id:8370, abbreviation:"NRL", slug:"3"`). Changing `3` to `nrl` makes the -endpoint 404 and the plugin silently stops fetching games. The code marks every -point where `3` is used with a comment to protect against this. - -## Scoring & period model - -NRL is played over two 40-minute halves (not quarters). ESPN reports -`status.period` as `1` or `2` and a running `displayClock` that counts **up** in -minutes (e.g. `40'`, `80'`), just like soccer. The plugin renders period text as: +This plugin is a single-league fork of `soccer-scoreboard` and keeps full +feature parity: switch or scroll display, live-game priority, try/win +celebrations, dynamic per-mode durations, and Vegas continuous-scroll support. -- `1H` / `2H` — first / second half (with the running clock, e.g. `2H 63'`) -- `HALF` — half-time -- `ET` — golden-point extra time (period ≥ 3) -- `Final` — completed game -- start time — upcoming games +> The crests above are grey placeholders because this documentation renders +> against a checkout with no cached logos. On a running display the real club +> crests are downloaded from ESPN's CDN on first sight and cached. -Each team has a single running integer score (the ESPN `score` field). +## Contents -## Team logos +- [Quick start](#quick-start) +- [Display modes](#display-modes) +- [How games are chosen](#how-games-are-chosen) +- [Panel sizes](#panel-sizes) +- [Settings reference](#settings-reference) +- [Matchup separator and the upcoming card middle](#matchup-separator-and-the-upcoming-card-middle) +- [Text colours](#text-colours) +- [Favorite team result colours](#favorite-team-result-colours) +- [Vegas ticker: seeing live games more often](#vegas-ticker-seeing-live-games-more-often) +- [Data source and the `3` league-slug quirk](#data-source-and-the-3-league-slug-quirk) +- [Scoring and period model](#scoring-and-period-model) +- [Team logos](#team-logos) +- [Troubleshooting](#troubleshooting) -Team logos are downloaded automatically from ESPN's CDN and cached locally — -there are no bundled logo assets to manage. If a download fails, a text -placeholder is generated from the team abbreviation. +## Quick start -## Configuration +1. Install **NRL Scoreboard** from the LEDMatrix Plugin Store. +2. Open its settings and add your clubs under **Favorite Teams**. +3. Save. The board picks the plugin up on the next rotation. -Configuration lives under the `nrl-scoreboard` key in `config/config.json`. Key -options (see `config_schema.json` for the full list, types, and defaults): +Use the **full team name** (`"Penrith Panthers"`) or the ESPN numeric team ID — +not the three-letter abbreviation. Two NRL abbreviations are ambiguous: `NEW` is +both Newcastle Knights and New Zealand Warriors, and `CAN` is both Canberra +Raiders and Canterbury Bulldogs. An ambiguous entry is left unresolved and +logged as an error rather than guessed at. -| Key | Description | -|---|---| -| `enabled` | Master on/off switch | -| `favorite_teams` | List of favorite NRL teams to prioritize. Use the full team name (e.g. `"Newcastle Knights"`) or ESPN team ID, **not** the 3-letter abbreviation — some abbreviations are shared by two teams (`NEW` is both Newcastle Knights and New Zealand Warriors; `CAN` is both Canberra Raiders and Canterbury Bulldogs). A shared abbreviation is left unresolved (logged as an error) rather than being matched to either team | -| `exclude_teams` | Teams to hide (spoiler protection). Same name/ID guidance as `favorite_teams` | -| `display_modes` | Toggle `live`/`recent`/`upcoming` and set `*_display_mode` to `switch` or `scroll` | -| `live_priority` | Interrupt the rotation to show live games immediately | -| `live_game_duration` / `recent_game_duration` / `upcoming_game_duration` | Per-game on-screen time (seconds) | -| `non_favorite_live_game_duration` | Shorter turn for live games without a favorite team | -| `recent_games_to_show` / `upcoming_games_to_show` | How many games per mode | -| `show_records` / `show_odds` / `show_ranking` | Extra info overlays | -| `celebration_enabled` / `celebration_duration` | Goal/win celebration takeover | -| `dynamic_duration` | Auto-size mode duration from the number of games | -| `mode_durations` | Fixed total time per mode | -| `update_interval_seconds` / `live_update_interval` | Data refresh cadence | -| `background_service` | Fetch timeout / retries / priority | -| `customization` | Fonts, colors, and layout for the scorebug | - -### Example +A minimal `config/config.json` entry: ```json { @@ -95,161 +58,334 @@ options (see `config_schema.json` for the full list, types, and defaults): "upcoming_display_mode": "scroll" }, "live_priority": true, - "recent_games_to_show": 3, - "upcoming_games_to_show": 5 + "game_limits": { + "recent_games_to_show": 3, + "upcoming_games_to_show": 5 + } } } ``` -### Timezone - -- `timezone` (Advanced): IANA name used to display event start times, e.g. - `America/Chicago`. Leave blank (the default) to follow the LEDMatrix global - timezone; if that isn't set, the host system's timezone is used, and only if - neither is available do times fall back to UTC. +## Display modes -## Favorite Team Result Colors +Three modes, each independently switchable, each declared in the manifest so the +core can schedule them separately. -A run of games against the same opponent is hard to read at a glance: in scroll -and Vegas mode the same two logos go past several times and only the digits -change. Turn on **Customization -> Favorite Team Result Colors** to color a -finished game's score by how your favorite team did - green for a win, red for -a loss. +![The three NRL display modes](../../docs/assets/nrl-scoreboard/display-modes.png) -```json -{ - "customization": { - "favorite_result_colors": { - "enabled": true, - "win_color": [0, 255, 0], - "loss_color": [255, 0, 0], - "tie_color": [255, 200, 0] - } - } -} -``` +| Mode | Shows | Top line | +|---|---|---| +| `nrl_live` | Games in progress | Period and running clock — `1H 22:10`, `2H 12:34`, `HALF`, `ET` | +| `nrl_recent` | Finished games | `Final`, or whatever period text the fixture ended on | +| `nrl_upcoming` | Scheduled games | `NRL`, then the kick-off date and time | -- Off by default. Until you enable it the score keeps exactly the color it has - today. -- Only finished games are colored. Live and upcoming cards are untouched. -- A game needs exactly one favorite team. If neither side is a favorite, or both - are, the score keeps its normal color. -- Applies to both the one-game-at-a-time switch view and the scroll/Vegas - ticker. -- The three colors are Advanced settings; leave them alone for the defaults - above. +Each mode renders as **switch** (one game at a time, timed) or **scroll** (all +games scroll horizontally at high FPS), set per mode with +`display_modes._display_mode`. -## 🎯 Which Games Get Shown +## How games are chosen -**`upcoming_games_to_show` is not "how many cards you see".** It is the size of a *pool*. The panel cycles through that pool one card at a time and keeps its place between visits, so a pool of 3 means the board rotates through the same 3 games until the schedule moves on. Making the number bigger gives you a *longer lap*, so any one game comes round **less** often. +**`upcoming_games_to_show` is not "how many cards you see".** It is the size of +a *pool*. The panel cycles through that pool one card at a time and keeps its +place between visits, so a pool of 3 means the board rotates through the same 3 +games until the schedule moves on. A bigger number gives you a *longer lap*, so +any one game comes round **less** often. -Which mode you are in depends on whether `favorite_teams` is set and whether `show_favorite_teams_only` is on: +Which of three regimes you are in depends on `favorite_teams` and +`show_favorite_teams_only`: | `favorite_teams` | `show_favorite_teams_only` | What you get | |---|---|---| -| empty | either | The next N games league-wide, chronologically. Every game shown is a non-favorite game, so the two filters below apply to all of them. | -| set | **on** | Only your teams. The limit is a budget **per team**. | -| set | **off** | **Your teams first, then other games to fill.** Both limits are **totals**. | - -The third row is what most people want, and it did not exist before: with the flag off, favorites used to be ignored *entirely*. +| empty | either | The next N games league-wide, chronologically. Every game is a non-favorite game, so the `other_*` filters apply to all of them. | +| set | **on** (default) | Only your clubs. The limit is a budget **per team**. | +| set | **off** | **Your clubs first, then other games to fill.** Both limits are **totals**. | -### The settings +### The selection settings | Option | Default | Description | |---|---|---| -| `upcoming_games_to_show` | varies | How many **favorite** upcoming games to show. | -| `recent_games_to_show` | varies | The same, for finished games. | -| `other_upcoming_games_to_show` | matches `upcoming_games_to_show` | How many **non-favorite** upcoming games to add. `0` gives you favorites only. | -| `other_recent_games_to_show` | matches `recent_games_to_show` | The same, for finished games. | +| `upcoming_games_to_show` | `1` | How many **favorite** upcoming games to pool. | +| `recent_games_to_show` | `1` | The same, for finished games. | +| `other_upcoming_games_to_show` | `1` | How many **non-favorite** upcoming games to add. `0` gives you favorites only. | +| `other_recent_games_to_show` | `1` | The same, for finished games. | | `other_rotation_interval_seconds` | `1800` | How often the non-favorite slice advances. `0` pins it. | -| `other_games_min_quality` | `ranked` | Which non-favorite games qualify: `ranked`, `broadcast`, or `any`. | -| `other_games_divisions` | `["fbs"]` | Which divisions non-favorite games may come from. College football only — see the note below. | +| `other_games_min_quality` | `ranked` | Which non-favorite games qualify. Inert here — see below. | +| `other_games_divisions` | `["fbs"]` | Which divisions non-favorite games may come from. Inert here — see below. | -**Your favorite teams are never filtered by the last two** — follow a smaller-division team and its games always appear. Those settings only decide what fills the *remaining* slots. +All seven are declared **twice**: at the root of the config and inside +`game_limits`. Both render in the web UI and both are read. **`game_limits` wins +where the key is present**, and the root value is used otherwise. Set one place +or the other, not both. -Within the other-games pool, **the better matchup leads**, and each team appears once. The pool is each team's *next* game ordered by the best poll position of either side, so a top-five matchup sits in the first window rather than whichever kicks off soonest — and the #1 team's whole season does not sort above everyone else's opener. Ties fall back to kickoff order, and a league with no poll keeps chronological order. Your favorite teams are ordered by when they play, not by rank -- for your own team the next game is the point. +Within the other-games pool the better matchup leads and each team appears once. +The pool is each team's *next* game ordered by the best poll position of either +side, with ties falling back to kick-off order. A league with no national poll — +which the NRL is — keeps plain chronological order. Your favorite clubs are +ordered by when they play, not by rank: for your own team the next game is the +point. ### Variety comes from turnover -Rather than widening the pool, the non-favorite slice **moves**: the window advances by its own width every `other_rotation_interval_seconds`, so consecutive windows do not overlap and the board works through the schedule instead of resampling the front of it. Your favorites are not rotated — for upcoming games the soonest ones are the point. +Rather than widening the pool, the non-favorite slice **moves**: the window +advances by its own width every `other_rotation_interval_seconds`, so +consecutive windows do not overlap and the board works through the schedule +instead of resampling the front of it. Your favorites are not rotated. -Both filters **fail open**: if the data behind them cannot be fetched, the game is allowed through. A board showing filler is a poor board; a board showing nothing is a broken one. +Both filters **fail open**: if the data behind them cannot be fetched, the game +is allowed through. They fail open a second time as a set — if the filters +between them leave nothing at all, the unfiltered list is used instead. Setting +`other_upcoming_games_to_show` or `other_recent_games_to_show` to `0` is the one +way to ask for an empty slate, and that is honoured. -They fail open a second time, as a set: if the filters between them leave **nothing at all** — your teams idle and every other game rejected — the unfiltered list is used instead. Setting `other_upcoming_games_to_show` or `other_recent_games_to_show` to `0` is the one way to ask for an empty slate, and that is honoured. +> **`other_games_min_quality` and `other_games_divisions` do nothing in this +> plugin.** `ranked` needs a national poll and the division filter needs ESPN's +> FBS/FCS group rosters; rugby league has neither, so every game passes both. +> Neither costs a request — no poll is fetched and no division lookup is made. +> They are present because the selection code is common to every scoreboard. -> Both `other_games_min_quality` and `other_games_divisions` are inert in this plugin. `ranked` needs a national poll and the division filter needs ESPN's FBS/FCS group rosters; this league has neither, so every game passes both, and neither costs a request — no poll is fetched and no division lookup is made. +### Live rotation +When several games are live at once the rotation is weighted, not a flat +round-robin: a game involving one of your clubs gets `favorite_live_boost` turns +for every one turn other live games get, and is queued first whenever the +rotation refreshes. Set it to `1` for even rotation. How long each game holds the +screen is `live_game_duration`, or `non_favorite_live_game_duration` for games +without a favorite when that is set above `0`. -## License +A live game that stops being reported by the API for `stale_game_timeout` +seconds is dropped from the rotation, so a game the feed abandons does not sit +on the board forever. -See `LICENSE`. +## Panel sizes -## Vegas ticker: seeing live games more often +The scorebug is laid out from the panel dimensions rather than a fixed grid, and +the plugin passes the render-safety harness on all eight supported sizes. -By default a live game **takes over** the display: the Vegas ticker stops and -this scoreboard shows full screen until the game ends. If you would rather keep -the marquee scrolling and still see scores, set this in the core config: +![NRL live card at four panel sizes](../../docs/assets/nrl-scoreboard/panel-sizes.png) -```json -{ - "display": { - "vegas_scroll": { - "live_in_ticker": true, - "live_weight": 3, - "favorite_live_weight": 5 - } - } -} -``` +At 64x32 the two crests and the centre column share very little room; a wider +panel is a much better fit for this card. -The ticker is otherwise a strict round robin — every plugin appears once per -cycle — so with a dozen plugins enabled a score comes round once a lap. These -weights let this plugin claim several slots per cycle, spaced evenly through -it rather than bunched together. +## Settings reference -`live_weight` applies whenever this scoreboard has a live game. -`favorite_live_weight` applies when one of your `favorite_teams` is playing, so -your team's game comes round more often than other live games. That distinction -has to be made here rather than in the core, which can tell *that* a game is -live but not *whose*. +Everything the plugin accepts. Settings marked **Advanced** sit behind the +*Advanced* toggle in the web UI. Defaults are the schema defaults, which is what +the web UI writes. -Two things to keep in mind: +### Core -- The weight is per **plugin**, not per game. With four games live this - scoreboard still occupies one slot at a time and picks between its own games - using `favorite_live_boost`; these weights control how often the scoreboard - itself comes round. -- More slots make the cycle **longer**, not faster — everything else appears - proportionally less often. And appearing more often only helps if the data is - fresh, which is governed by this plugin's own live update interval. +| Key | Type | Default | What it does | +|---|---|---|---| +| `enabled` | boolean | `true` | Master on/off switch. | +| `favorite_teams` | array | `[]` | Clubs to prioritise. Full name or ESPN team ID. | +| `exclude_teams` | array | `[]` | Clubs to always hide, from live rotation and finals alike (spoiler protection). Takes precedence over `favorite_teams` and `show_all_live`. | +| `live_priority` | boolean | `true` | Let live games interrupt the normal mode rotation and display immediately. | +| `timezone` | string | `""` | **Advanced.** IANA zone for start times, e.g. `Australia/Sydney`. Blank follows the LEDMatrix global timezone, then the host system's, then UTC. | + +### Display modes + +| Key | Type | Default | What it does | +|---|---|---|---| +| `display_modes.live` | boolean | `true` | Show live games. | +| `display_modes.recent` | boolean | `true` | Show recently completed games. | +| `display_modes.upcoming` | boolean | `true` | Show scheduled games. | +| `display_modes.live_display_mode` | `switch` \| `scroll` | `switch` | One game at a time, or all games scrolling. | +| `display_modes.recent_display_mode` | `switch` \| `scroll` | `switch` | As above, for finished games. | +| `display_modes.upcoming_display_mode` | `switch` \| `scroll` | `switch` | As above, for scheduled games. | + +`display_modes` sets `additionalProperties: false`, so a misspelled key here is +rejected outright rather than quietly ignored. + +### Timing + +| Key | Type | Default | What it does | +|---|---|---|---| +| `live_game_duration` | 10–120 s | `20` | How long each live game holds the screen. Applies to favorites' games when a separate non-favorite duration is set; to all live games when no favorites are configured. | +| `non_favorite_live_game_duration` | 0–120 s | `0` | **Advanced.** Shorter turn for live games without a favorite. Only applies when favorites are set **and** non-favorite live games are shown. `0` means use `live_game_duration` for everything. | +| `recent_game_duration` | 5–60 s | `15` | Per-game time on the Recent screen. | +| `upcoming_game_duration` | 5–60 s | `15` | Per-game time on the Upcoming screen. | +| `game_display_duration` | 3–60 s | `15` | **Advanced.** Generic per-game fallback where a mode-specific duration is not set. | +| `display_duration` | 5–60 s | `15` | **Advanced.** Legacy per-game duration, superseded by the three above. | + +### Mode durations + +How long the *whole mode* holds the board before the core rotates to the next +plugin. Leave at `null` to let dynamic duration decide. + +| Key | Type | Default | +|---|---|---| +| `mode_durations.live_mode_duration` | 10–600 s or `null` | `null` | +| `mode_durations.recent_mode_duration` | 10–600 s or `null` | `null` | +| `mode_durations.upcoming_mode_duration` | 10–600 s or `null` | `null` | + +All three are **Advanced**. They are read by the LEDMatrix core rather than by +this plugin, which is why they do not appear in the plugin's own source. + +### Dynamic duration + +Sizes each mode's total time from how much there is to show, instead of a fixed +number. + +| Key | Type | Default | What it does | +|---|---|---|---| +| `dynamic_duration.enabled` | boolean | `false` | **Advanced.** Master switch for the plugin. | +| `dynamic_duration.min_duration_seconds` | 10–300 s | `30` | **Advanced.** Floor, even with few games. | +| `dynamic_duration.max_duration_seconds` | 60–600 s | — | **Advanced.** Ceiling. | +| `dynamic_duration.modes.live.enabled` | boolean | `false` | **Advanced.** Per-mode override. | +| `dynamic_duration.modes.live.max_duration_seconds` | 60–600 s | — | **Advanced.** | +| `dynamic_duration.modes.recent.enabled` | boolean | `false` | **Advanced.** | +| `dynamic_duration.modes.recent.max_duration_seconds` | 60–600 s | — | **Advanced.** | +| `dynamic_duration.modes.upcoming.enabled` | boolean | `false` | **Advanced.** | +| `dynamic_duration.modes.upcoming.max_duration_seconds` | 60–600 s | — | **Advanced.** | + +### Filtering + +| Key | Type | Default | What it does | +|---|---|---|---| +| `show_favorite_teams_only` | boolean | `true` | Show only your clubs' games. | +| `filtering.show_favorite_teams_only` | boolean | `true` | **Advanced.** The same setting in its nested home; this copy wins when the `filtering` object is present. | +| `filtering.show_all_live` | boolean | `false` | **Advanced.** Show every live game regardless of favorites. `exclude_teams` still applies. | +| `filtering.favorite_live_boost` | 1–5 | `2` | **Advanced.** Turns a favorite's live game gets per one turn for other live games. `1` is even rotation. | + +### Overlays + +| Key | Type | Default | What it does | +|---|---|---|---| +| `show_records` | boolean | `false` | Draw each club's win-loss record in the bottom corners. | +| `show_ranking` | boolean | `false` | Draw ladder positions where ESPN publishes them. | +| `show_odds` | boolean | `true` | Draw betting odds. | +| `display_options.show_records` | boolean | `false` | **Advanced.** Nested copy; wins over the root key when present. | +| `display_options.show_ranking` | boolean | `false` | **Advanced.** Nested copy. | +| `display_options.show_odds` | boolean | `true` | **Advanced.** Nested copy. | + +![show_records on and off](../../docs/assets/nrl-scoreboard/show-records.png) + +### Celebrations + +| Key | Type | Default | What it does | +|---|---|---|---| +| `celebration_enabled` | boolean | `true` | Full-screen takeover when a favorite scores or wins a live game. | +| `celebration_duration` | 3–30 s | `8` | **Advanced.** How long the takeover stays up. | +| `celebrate_opponent_goals` | boolean | `false` | **Advanced.** Also celebrate the opponent's points. | + +A celebration owns the screen while it runs: the live rotation does not advance +underneath it, and the dwell timer resets afterwards so the scoring game gets a +full turn before the board moves on. + +### Fetching + +| Key | Type | Default | What it does | +|---|---|---|---| +| `update_interval_seconds` | 30–86400 s | `3600` | **Advanced.** Base data refresh cadence. | +| `live_update_interval` | 5–300 s | `30` | **Advanced.** Refresh cadence while a game is live. | +| `recent_update_interval` | 60–86400 s | `3600` | **Advanced.** Refresh cadence for finished games. | +| `upcoming_update_interval` | 60–86400 s | `3600` | **Advanced.** Refresh cadence for the schedule. | +| `stale_game_timeout` | 60–3600 s | `300` | **Advanced.** Drop a live game the API has stopped updating. | +| `no_data_interval_seconds` | 5–86400 s | `300` | **Advanced.** Wait between live checks when there are no live games. Backs off further the longer nothing is found. | +| `live_idle_max_interval_seconds` | 5–86400 s | `900` | **Advanced.** Ceiling for that back-off. | +| `schedule_lookback_days` | 1–60 | `14` | **Advanced.** How far back to fetch for the Recent screen. | +| `schedule_lookahead_days` | 1–60 | `7` | **Advanced.** How far ahead to fetch for Upcoming. A fixture beyond this horizon is never fetched, so it cannot reach the board even though the date is known. | + +### Background service + +All **Advanced**; the defaults suit a Pi and rarely want changing. + +| Key | Type | Default | +|---|---|---| +| `background_service.enabled` | boolean | `true` | +| `background_service.max_workers` | 1–10 | `3` | +| `background_service.request_timeout` | 5–120 s | `30` | +| `background_service.max_retries` | 1–10 | `3` | +| `background_service.priority` | 1–5 | `2` | + +`background_service` sets `additionalProperties: false`. + +### Fonts and sizes + +Seven text elements, each with `font`, `font_size`, and `text_color`, under +`customization.`. Available faces: `PressStart2P-Regular.ttf`, +`4x6-font.ttf`, `5by7.regular.ttf`, `5x7.bdf`, `4x6.bdf`, `cozette.bdf`. + +| Element | Default font | Default size | Draws | +|---|---|---|---| +| `score_text` | `PressStart2P-Regular.ttf` | `10` | The score, and the matchup separator on an upcoming card | +| `period_text` | `PressStart2P-Regular.ttf` | `8` | The clock and period, and the date/time on an upcoming scoreboard | +| `team_name` | `PressStart2P-Regular.ttf` | `8` | Team names and abbreviations | +| `status_text` | `4x6-font.ttf` | `6` | Status lines such as "Next Game" | +| `detail_text` | `4x6-font.ttf` | `6` | Small detail lines | +| `rank_text` | `PressStart2P-Regular.ttf` | `10` | Ladder positions | +| `odds_text` | `4x6-font.ttf` | `6` | Betting odds (defaults to green, `[0, 255, 0]`) | + +The `.bdf` faces are bitmap fonts that exist at exactly one pixel size; sizes +snap to that grid to stay crisp rather than being scaled. Every +`customization.` object sets `additionalProperties: false`. + +### Layout offsets + +Nudge any element in pixels. All default to `0`, all live under +`customization.layout.`, and all set `additionalProperties: false`. + +| Element | Keys | Measured from | +|---|---|---| +| `home_logo`, `away_logo` | `x_offset`, `y_offset` | Default logo position | +| `score` | `x_offset`, `y_offset` | Panel centre | +| `status_text` | `x_offset`, `y_offset` | Centre horizontally, top vertically | +| `date` | `x_offset`, `y_offset` | Centre horizontally, default position vertically | +| `time` | `x_offset`, `y_offset` | Centre horizontally, the date's position vertically | +| `records` | `away_x_offset`, `home_x_offset`, `y_offset` | Away from the left, home from the right, both from the bottom | +| `odds` | `x_offset`, `y_offset` | **Advanced.** Default odds position | + +### Scroll settings + +| Key | Type | Default | +|---|---|---| +| `scroll_settings.scroll_speed` | 0.01–200 px/s | `1.0` | +| `scroll_settings.scroll_delay` | 0.001–0.1 s | `0.01` | +| `scroll_settings.gap_between_games` | 8–128 px | `48` | +| `scroll_settings.show_league_separators` | boolean | `true` | +| `scroll_settings.dynamic_duration` | boolean | `true` | +| `scroll_settings.game_card_width` | 32–512 px | `128` | + +All **Advanced**. + +> **These six settings currently have no effect.** The scroll renderer reads a +> `scroll_mode` object that the schema does not declare, and the schema will not +> accept `scroll_mode` if you add it by hand — so the block is unreachable from +> the UI and from a hand-written config alike. Tracked as +> [issue #422](https://github.com/ChuckBuilds/ledmatrix-plugins/issues/422). +> Scroll display itself works; only these tuning knobs are inert. ## Matchup separator and the upcoming card middle -The **Matchup Card Layout** section (advanced) controls what sits between the -two team logos before a game starts, and how the date and time are written. -These settings now apply to every display mode -- the scroll ticker, the Vegas -ticker, and the full-screen scoreboard -- rather than only the tickers. +The **Matchup Card Layout** section controls what sits between the two crests +before a game starts, and how the date and time are written. These settings +apply to every display mode — the scroll ticker, the Vegas ticker, and the +full-screen scoreboard. | Setting | Key | Default | What it does | |---|---|---|---| -| Matchup Separator | `vs_text` | `VS` | Text drawn between the teams: `VS`, `@`, `at`, `v`. The away team is always on the left, so `@` and `at` read as "away at home". Blank draws nothing. | -| Middle of an Upcoming Card | `upcoming_center` | `vs` | Scroll and Vegas cards: the separator, the date and time stacked, or nothing. | -| Middle of a Full-Screen Upcoming Scoreboard | `switch_upcoming_center` | `date_time` | The same choice for the full-screen scoreboard, plus `inherit` to follow the setting above. It defaults to the stacked date and time, which is what this display has always shown, so nothing changes until you pick something else. | -| Date Format | `date_format` | `abbrev` | How the scroll and Vegas cards write the date: `Sep 19`, `9/19`, `19 Sep`, `19/9`, or `Fri Sep 19`. | -| Full-Screen Date Format | `switch_date_format` | `numeric` | The same choice for the full-screen scoreboard, plus `inherit` to follow the row above. It has its own default because the two displays disagree about what is normal: the cards have always written `Sep 19` and the full-screen scoreboard `9/19`, so a single shared default would restyle one of them. | -| Time Format | `time_format` | `12h` | 12- or 24-hour clock. | -| Show Date / Show Time | `show_date`, `show_time` | `true` | Drop either line. | -| Swap Date and Time | `swap_date_time` | `false` | Swap the two lines over. Each display starts from its own order, so this flips them rather than forcing one: the scroll and Vegas cards put the time on top, the full-screen date/time stack puts the date on top. | +| Matchup Separator | `scroll_card.vs_text` | `VS` | Text between the teams: `VS`, `@`, `at`, `v`. The away side is always on the left, so `@` and `at` read as "away at home". Blank draws nothing. | +| Middle of an Upcoming Card | `scroll_card.upcoming_center` | `vs` | Scroll and Vegas cards: `vs`, `date_time`, or `none`. | +| Middle of a Full-Screen Upcoming Scoreboard | `scroll_card.switch_upcoming_center` | `date_time` | The same choice for the full-screen scoreboard, plus `inherit` to follow the row above. | +| Date Format | `scroll_card.date_format` | `abbrev` | Scroll and Vegas cards: `Sep 19`, `9/19`, `19 Sep`, `19/9`, or `Fri Sep 19`. | +| Full-Screen Date Format | `scroll_card.switch_date_format` | `numeric` | **Advanced.** The same for the full-screen scoreboard, plus `inherit`. It has its own default because the two displays disagree about what is normal: the cards have always written `Sep 19` and the full-screen scoreboard `9/19`, so a single shared default would restyle one of them. | +| Time Format | `scroll_card.time_format` | `12h` | 12- or 24-hour clock. | +| Show Date / Show Time | `scroll_card.show_date`, `scroll_card.show_time` | `true` | Drop either line. | +| Swap Date and Time | `scroll_card.swap_date_time` | `false` | Flip the two lines. Each display starts from its own order, so this flips rather than forces: scroll and Vegas cards put the time on top, the full-screen stack puts the date on top. | Choosing the separator for the full-screen scoreboard moves the date and time -out of the middle and onto the top and bottom rows, the same way the scroll -card lays them out; the "Next Game" header gives up the top row to them. +out of the middle and onto the top and bottom rows, the way the scroll card lays +them out; the "Next Game" header gives up the top row to them. -The center-gap settings in the same section size the scroll and Vegas card's -middle strip only -- the full-screen scoreboard pins its logos to the panel -edges and is unaffected. +The centre-gap settings size the scroll and Vegas card's middle strip only — the +full-screen scoreboard pins its crests to the panel edges and is unaffected. -Example: +| Key | Type | Default | What it does | +|---|---|---|---| +| `scroll_card.center_gap` | 0–64 px | unset | Pixels kept clear down the middle. Unset scales with card width; `0` restores edge-to-edge logos. | +| `scroll_card.center_gap_ratio` | 0.0–0.6 | `0.28` | **Advanced.** Fraction of card width used when the gap is not pinned. | +| `scroll_card.center_gap_min` | 0–64 px | `22` | **Advanced.** Floor for the scaled gap. | +| `scroll_card.center_gap_max` | 0–96 px | `40` | **Advanced.** Ceiling for the scaled gap. | ```json { @@ -261,24 +397,12 @@ Example: } ``` -### Text Colours - -Each text element in the **Customization** section carries a colour, and it now -applies to the text drawn in that element's face — on the full-screen scoreboard -and on the scroll and Vegas cards alike. Until this version the picker changed -only which font was loaded; every string was drawn white. - -| Element | Key | Colours | -|---|---|---| -| Score | `score_text` | The score, and the matchup separator on an upcoming card | -| Period / clock | `period_text` | The clock, period, and the date and time on an upcoming scoreboard | -| Team name | `team_name` | Team names and abbreviations | -| Status | `status_text` | Status lines such as "Next Game" | -| Detail | `detail_text` | Small detail lines | -| Ranking | `rank_text` | Team rankings drawn in the ranking face | +## Text colours -Colours are `[r, g, b]` or `"#RRGGBB"`, and every default is white, so a display -nobody has recoloured looks exactly as it did. +Each `customization..text_color` colours the text drawn in that +element's face, on the full-screen scoreboard and on the scroll and Vegas cards +alike. Colours are `[r, g, b]` or `"#RRGGBB"`. Every default is white except +`odds_text`, which is green. ```json { @@ -291,6 +415,148 @@ nobody has recoloured looks exactly as it did. Two things keep their own colours on purpose: the betting-odds figures, which are coloured by which side is favoured, and a finished game's score when -**Favorite Team Result Colors** is on — that tint wins, and your score colour -shows on every other game. Records and rankings drawn in the small fixed face -stay white; no element in the schema owns that face. +**Favorite Team Result Colors** is on — that tint wins. Records and rankings +drawn in the small fixed face stay white; no element in the schema owns that +face. + +## Favorite team result colours + +A run of games against the same opponent is hard to read at a glance: in scroll +and Vegas mode the same two crests go past several times and only the digits +change. Turn this on to colour a finished game's score by how your club did. + +| Key | Type | Default | +|---|---|---| +| `customization.favorite_result_colors.enabled` | boolean | `false` | +| `customization.favorite_result_colors.win_color` | `[r, g, b]` | `[0, 255, 0]` | +| `customization.favorite_result_colors.loss_color` | `[r, g, b]` | `[255, 0, 0]` | +| `customization.favorite_result_colors.tie_color` | `[r, g, b]` | `[255, 200, 0]` | + +```json +{ + "customization": { + "favorite_result_colors": { + "enabled": true, + "win_color": [0, 255, 0], + "loss_color": [255, 0, 0], + "tie_color": [255, 200, 0] + } + } +} +``` + +- Only finished games are coloured; live and upcoming cards are untouched. +- A game needs **exactly one** favorite club. Neither side or both, and the score + keeps its normal colour. +- Applies to the switch view and the scroll/Vegas ticker alike. +- The three colours are Advanced settings. + +This tint is applied by the LEDMatrix core rather than by the plugin, which is +why the keys do not appear in this plugin's source. + +## Vegas ticker: seeing live games more often + +By default a live game **takes over** the display: the Vegas ticker stops and +this scoreboard shows full screen until the game ends. To keep the marquee +scrolling and still see scores, set this in the **core** config — not in this +plugin's settings: + +```json +{ + "display": { + "vegas_scroll": { + "live_in_ticker": true, + "live_weight": 3, + "favorite_live_weight": 5 + } + } +} +``` + +The ticker is otherwise a strict round robin — every plugin appears once per +cycle — so with a dozen plugins enabled a score comes round once a lap. These +weights let this plugin claim several slots per cycle, spaced evenly through it +rather than bunched together. + +`live_weight` applies whenever this scoreboard has a live game. +`favorite_live_weight` applies when one of your `favorite_teams` is playing. That +distinction has to be made here rather than in the core, which can tell *that* a +game is live but not *whose*. + +- The weight is per **plugin**, not per game. With four games live this + scoreboard still occupies one slot at a time and picks between its own games + using `favorite_live_boost`. +- More slots make the cycle **longer**, not faster — everything else appears + proportionally less often. And appearing more often only helps if the data is + fresh, which is governed by `live_update_interval`. + +## Data source and the `3` league-slug quirk + +``` +https://site.api.espn.com/apis/site/v2/sports/rugby-league/3/scoreboard +``` + +Note the `3` in the path. That is **ESPN's internal numeric slug for the NRL** +under the `rugby-league` sport — not a typo, and it must **not** be changed to +`nrl`. The human-facing web path is `/nrl/`, but the API path segment is the +literal string `3` (confirmed via +`site.web.api.espn.com/apis/v2/scoreboard/header?sport=rugby-league`, which +lists NRL as `id:8370, abbreviation:"NRL", slug:"3"`). Changing `3` to `nrl` +makes the endpoint 404 and the plugin silently stops fetching games. Every place +the code uses `3` carries a comment saying so. + +No API key is required. + +## Scoring and period model + +The NRL plays two 40-minute halves, not quarters. ESPN reports `status.period` +as `1` or `2` with a `displayClock` that counts **up** in minutes, like soccer. +The plugin renders the period text as: + +| Text | Meaning | +|---|---| +| `1H` / `2H` with the clock | First or second half, e.g. `2H 12:34` | +| `HALF` | Half-time | +| `ET` | Golden-point extra time (period ≥ 3) | +| `Final` | Completed game | +| Start time | Upcoming game | + +Each club carries a single running integer score. + +## Team logos + +Crests are downloaded from ESPN's CDN on first sight and cached locally — there +are no bundled logo assets to manage. If a download fails, a placeholder is +generated from the team abbreviation, which is what the images in this document +show. + +## Troubleshooting + +**Nothing appears.** Check that `enabled` is `true` and at least one entry in +`display_modes` is on. With `show_favorite_teams_only` at its default of `true` +and no `favorite_teams` set, there is nothing to select from. + +**A club I follow never shows up.** Confirm you used the full team name or the +ESPN ID, not the abbreviation — `NEW` and `CAN` are each shared by two clubs and +are deliberately left unresolved. The log records an error naming the entry it +could not resolve. + +**A fixture I know about never appears.** It may be beyond +`schedule_lookahead_days` (default 7). A fixture outside that horizon is never +fetched. + +**The same few games keep repeating.** That is the pool cycling. Lower +`other_rotation_interval_seconds` for faster turnover rather than raising the +pool size — a larger pool makes the lap longer, so each game appears less often, +not more. + +**A finished game disappeared too soon.** Raise `schedule_lookback_days` +(default 14). + +**Changes in the UI seem to do nothing.** Check whether you set the root copy of +a key that also exists under `game_limits`, `display_options`, or `filtering`. +The nested copy wins where it is present. + +## License + +See `LICENSE`. diff --git a/plugins/nrl-scoreboard/manifest.json b/plugins/nrl-scoreboard/manifest.json index a2b2a297..24ed7135 100644 --- a/plugins/nrl-scoreboard/manifest.json +++ b/plugins/nrl-scoreboard/manifest.json @@ -1,7 +1,7 @@ { "id": "nrl-scoreboard", "name": "NRL Scoreboard", - "version": "1.21.1", + "version": "1.21.2", "author": "ChuckBuilds", "description": "Live, recent, and upcoming NRL (National Rugby League) games with real-time scores and game status.", "category": "sports", @@ -18,6 +18,16 @@ "nrl_upcoming" ], "versions": [ + { + "version": "1.21.2", + "released": "2026-09-02", + "ledmatrix_min_version": "3.3.0", + "notes": "Fix every grid-snapped font rendering a pixel narrow. The shared code reads this plugin's config_schema.json to tell a default font size from one the user chose; a default gets snapped to the font's pixel grid, a choice is left alone. It located the schema by inspecting loaded modules, which fails under the real plugin loader -- the loader renames a plugin's modules and removes their original names, so nothing was left to inspect. The lookup returned nothing, every size then looked user-chosen, and the snap was skipped: 4x6-font.ttf drew at 6 instead of 7, which is 3-pixel-wide glyphs instead of 4. On a 256x64 panel the betting odds, team records and the date row were hard to read. The plugin now tells the shared code where it lives instead of leaving it to guess.", + "changelog": "Retry a team logo whose previous download failed, instead of showing a grey box forever. A failed download is cached by the core as a placeholder wearing the real logo's filename; the logo loader scans filename variations, found that stub, and so never called the downloader again. The loader now skips a placeholder that is stale enough to be worth retrying and lets the download run, which also picks up stubs already on disk. The retry is rate-limited by the core (6h), so this does not trade a permanent grey box for a request every frame. Needs a core carrying src.logo_downloader.is_placeholder_logo; against an older core the check is skipped and behaviour is unchanged. Ported byte-identically across every sports lineage.", + "changes": [ + "Rewrote the README as a complete settings reference covering all 131 settings, with rendered examples of every display mode." + ] + }, { "version": "1.21.1", "released": "2026-09-02", diff --git a/scripts/docs_render_support/sitecustomize.py b/scripts/docs_render_support/sitecustomize.py index 44eafa3a..64fbb438 100644 --- a/scripts/docs_render_support/sitecustomize.py +++ b/scripts/docs_render_support/sitecustomize.py @@ -89,12 +89,59 @@ def advance(seconds): _WANTED = _json.loads(_ATTRS) _TARGET = "src.plugin_system.plugin_loader" + def _resolve(instance, path): + """Walk a dotted path to the object that owns the final attribute. + + The sports scoreboards keep their per-mode state on sub-managers rather + than on the plugin -- self._managers["live"].current_game -- so a shot + that can only set top-level attributes cannot reach the game it wants + to draw. A segment is tried as a mapping key first, then as an + attribute, so both self._managers["live"] and self.live_manager work. + """ + parts = path.split(".") + target = instance + for part in parts[:-1]: + if hasattr(target, "get") and not hasattr(target, part): + nxt = target.get(part) + else: + nxt = getattr(target, part, None) + if nxt is None and hasattr(target, "get"): + nxt = target.get(part) + if nxt is None: + return None, None + target = nxt + return target, parts[-1] + + def _coerce(name, value): + """Turn JSON into the shapes the plugins actually hold. + + A shots file can only carry JSON, but plugin state is not all strings + and lists: colours are tuples throughout the core, and a logo field is + a pathlib.Path -- the sports renderers call logo_path.parent, so a + string there raises AttributeError and the card silently fails to draw. + """ + if isinstance(value, list) and len(value) == 3 and all( + isinstance(v, int) for v in value): + return tuple(value) # colours are tuples everywhere in the core + if isinstance(value, str) and name.endswith("_path") and value: + import pathlib as _pathlib + return _pathlib.Path(value) + if isinstance(value, dict): + return {k: _coerce(k, v) for k, v in value.items()} + if isinstance(value, list): + return [_coerce(name, v) for v in value] + return value + def _apply(instance): for name, value in _WANTED.items(): - if isinstance(value, list) and len(value) == 3 and all( - isinstance(v, int) for v in value): - value = tuple(value) # colours are tuples everywhere in the core - setattr(instance, name, value) + value = _coerce(name.rsplit(".", 1)[-1], value) + owner, attr = _resolve(instance, name) + if owner is None: + continue # the path does not exist on this plugin; leave it alone + if hasattr(owner, "__setitem__") and not hasattr(owner, attr): + owner[attr] = value + else: + setattr(owner, attr, value) return instance class _PatchingLoader: diff --git a/scripts/render_docs_assets.py b/scripts/render_docs_assets.py index fa38f5f9..2ca204e6 100644 --- a/scripts/render_docs_assets.py +++ b/scripts/render_docs_assets.py @@ -268,6 +268,9 @@ def render_shot( mock_path = resolve_mock_data(mock_spec, shot_list_dir, tmpdir, name) if mock_path: cmd += ["--mock-data", str(mock_path)] + display_mode = shot.get("display_mode", defaults.get("display_mode")) + if display_mode: + cmd += ["--display-mode", str(display_mode)] if shot.get("skip_update", defaults.get("skip_update", False)): cmd.append("--skip-update")