feat(vegas): let live content keep its place in the ticker - #457
Conversation
Live content used to preempt Vegas outright: while any plugin reported live priority the display controller refused to run the ticker at all and showed a full-screen scoreboard instead. Keeping the marquee meant not seeing live scores; seeing live scores meant losing the marquee. Two changes, both off by default. vegas_scroll.live_in_ticker keeps the ticker running through a live game. Three places assumed the takeover and all three now honour it: the controller's gate, the coordinator's per-frame pause, and the rotation switch that would otherwise move current_mode_index underneath a ticker that never yields. And the rotation is no longer a strict round robin. It was one slot per plugin per cycle, so with a dozen plugins enabled a live score came round once a lap and could be minutes old on screen. A plugin can now hold several slots, placed by Smooth Weighted Round-Robin -- the same scheduler the sports plugins already use to rotate their own games. The property that matters is that repeats are spread through the cycle rather than clumped: three in a row and then silence would be worse than no boost at all. Weight comes from the plugin first, via a new optional get_vegas_priority_weight(), then from the core: live content earns live_weight, everything else 1. So existing plugins gain the behaviour without changes, and the hook exists for the one thing the core cannot work out -- the core can see that a game is live but not whose, so only the plugin can say a favorite is playing. Documented in ADVANCED_FEATURES (worked example, why weights are per plugin not per game, and that frequency is not freshness), CONFIG_REFERENCE, PLUGIN_API_REFERENCE, and the config template. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 38 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
📝 WalkthroughWalkthroughVegas mode adds live ticker configuration, plugin-defined priority weights, and smooth weighted round-robin scheduling. Live content can remain in the ticker instead of preempting Vegas mode. Documentation and tests cover configuration, weighting, fallback, distribution, and parsing. ChangesVegas live ticker
Estimated code review effort: 3 (Moderate) | ~25 minutes Mergeability Score: 🔵 Low · up to The change is mergeable with owner follow-up: a schedule-repair edge case can place the same item in adjacent ticker slots, reducing the intended spread of boosted content. Excluding that candidate and adding a regression test would close the bounded display-quality risk. Sequence Diagram(s)sequenceDiagram
participant VegasModeConfig
participant BasePlugin
participant StreamManager
participant VegasModeCoordinator
participant DisplayController
VegasModeConfig->>StreamManager: provide live ticker settings
BasePlugin->>StreamManager: provide optional plugin weight
StreamManager->>StreamManager: resolve weights and build rotation
DisplayController->>VegasModeCoordinator: check live priority
VegasModeCoordinator->>VegasModeConfig: read live_in_ticker
VegasModeConfig-->>VegasModeCoordinator: return ticker setting
VegasModeCoordinator-->>DisplayController: continue ticker when enabled
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ADVANCED_FEATURES.md`:
- Around line 67-69: Update the `display.vegas_scroll` key count in the
referenced documentation text from 29 to 30, leaving the surrounding explanation
and `CONFIG_REFERENCE.md` link unchanged.
In `@src/vegas_mode/config.py`:
- Around line 143-152: Update the configuration class’s to_dict() and update()
methods to include live_in_ticker, live_weight, and favorite_live_weight. Ensure
get_status() receives all three serialized values, allow partial updates to
modify them, and apply the same weight clamping used by from_config() when
updating live_weight and favorite_live_weight.
In `@src/vegas_mode/stream_manager.py`:
- Around line 443-445: Update the get_vegas_priority_weight() exception handler
in src/vegas_mode/stream_manager.py (lines 443-445) to return weight 1 after
logging the hook failure. In test/test_vegas_live_weighting.py (lines 25-43),
separate hook exceptions from live-content exceptions; add coverage at lines
94-96 for a live plugin whose hook alone raises and assert that its weight is 1.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2e8044f4-fb1d-4dd1-a6ee-27a1ca985ddd
📒 Files selected for processing (10)
config/config.template.jsondocs/ADVANCED_FEATURES.mddocs/CONFIG_REFERENCE.mddocs/PLUGIN_API_REFERENCE.mdsrc/display_controller.pysrc/plugin_system/base_plugin.pysrc/vegas_mode/config.pysrc/vegas_mode/coordinator.pysrc/vegas_mode/stream_manager.pytest/test_vegas_live_weighting.py
Three findings from CodeRabbit, all valid. to_dict() and update() enumerate keys explicitly and had not learned the three new ones, so get_status() never reported them and a live config change never applied -- turning live_in_ticker on in the web UI would have done nothing until a restart. update() clamps the weights exactly as from_config does. The vegas_scroll key count in ADVANCED_FEATURES said 29; the template has 30. My arithmetic, not the reviewer's. The third was a documentation error rather than a code one, and I have fixed it the other way round. The docs claimed a raising get_vegas_priority_weight() is treated as weight 1. The code instead falls through to the core's own live-content check, and that is the better behaviour: the hook is only how a plugin asks for *more* than live_weight, and has_live_priority/has_live_content are separate methods guarded separately, so a plugin with a broken weight calculation should lose the favorite distinction and keep the live boost. Said so in the code, the base-plugin docstring and the API reference. The test fake now fails in each place independently, because the two failures mean different things: a broken hook still earns live_weight, a plugin that cannot say whether it is live has nothing to fall back on and weighs 1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ui/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Smooth Weighted Round-Robin spaces repeats well within a pass, but it schedules the heaviest item first and usually last as well. The strip loops, so those two are neighbours: the marquee showed the same plugin twice running at exactly the one join a within-cycle check cannot see. Observed on a live rig at 28 slots -- gaps of 6, 7, 7, 7 and then 1. Rotating the list does not fix it. Rotation preserves the cyclic order exactly, so it moves where the seam is drawn rather than the adjacency itself; the trailing entry has to be swapped with one from the middle. The first version swapped with the first slot that merely fitted, which undid the spacing this exists to protect -- it moved a repeat from a gap of 7 into a gap of 2, more clumped than the seam had ever been. It now picks the candidate furthest from any other appearance, so the repeat lands in the widest gap. Left alone when no candidate exists. A plugin holding most of the slots has to neighbour itself, and scheduling it is better than refusing to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/vegas_mode/stream_manager.py`:
- Around line 528-543: Update the candidate selection in the seam-repair logic
to exclude entries whose value matches schedule[-2], preventing the swap from
creating an adjacent duplicate at the end. Add a direct regression test covering
the provided schedule and verify the repaired schedule does not end with
identical adjacent values.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e0b321c6-73f1-46fe-843c-f91a30e8ccb4
📒 Files selected for processing (6)
docs/ADVANCED_FEATURES.mddocs/PLUGIN_API_REFERENCE.mdsrc/plugin_system/base_plugin.pysrc/vegas_mode/config.pysrc/vegas_mode/stream_manager.pytest/test_vegas_live_weighting.py
🚧 Files skipped from review as they are similar to previous changes (3)
- src/plugin_system/base_plugin.py
- docs/PLUGIN_API_REFERENCE.md
- docs/ADVANCED_FEATURES.md
Swapping the trailing repeat with a middle slot moves two elements, and the candidate filter only guarded one of them. It checked the neighbours `repeated` would acquire at j, but not what the displaced element would sit beside at the end -- so ['a','b','c','d','x','y','x','a'] came back as [...,'x','x'], the seam duplicate traded for a fresh one. Reported by CodeRabbit with that exact case. Adding the missing condition fixed it and immediately broke something else: schedule[j] is schedule[-2] when j is the second-to-last slot, so that candidate was always excluded, and ['a','b','c','a'] lost the only repair it has. The same class of mistake twice, from reasoning about which neighbours two moved elements end up with. So it no longer reasons. It performs each candidate swap, counts the cyclic duplicates in the result, and keeps the best one that has none -- preferring whichever leaves the boosted plugin most evenly spread. When no such swap exists the schedule is returned untouched, which is the unavoidable case: a plugin holding most of the slots has to neighbour itself. Fuzzed across 6,956 seam schedules: none made worse, none lost an entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
|
Both threads addressed. Seam repair — real bug, exactly as described. Adding the suggested So it no longer reasons: it performs each candidate swap, counts cyclic duplicates in the result, and keeps the best swap that has none. Fuzzed over 6,956 seam schedules — none made worse, none lost an entry. Three regression tests, two of which fail against the previous repair. Hook-exception contract — the contradiction was real, but it was in the PR description, not the code. That line still read "a raising hook is caught and treated as 1"; the code and docs deliberately do something else, and I had not updated the description when I changed it. Corrected there. The behaviour is intentional: the hook is only how a plugin asks for more than That is already pinned by three tests: |
The problem
Live content preempts Vegas outright. While any plugin reports live priority,
display_controller.pyrefuses to run the ticker and shows a full-screen scoreboard instead. So keeping the marquee meant not seeing live scores, and seeing live scores meant losing the marquee.And even if the ticker did run, it wouldn't help much: the rotation is a strict round robin, one slot per plugin per cycle. With a dozen plugins enabled a live score comes round once a lap and can be minutes stale on screen.
Two changes, both off by default
vegas_scroll.live_in_tickerkeeps the ticker running through a live game. Three places assumed the takeover and all three now honour it — the controller's gate, the coordinator's per-framepause(), and the rotation switch that would otherwise movecurrent_mode_indexand stash a resume point underneath a ticker that never yields.Weighted rotation. A plugin can hold several slots per cycle, placed by Smooth Weighted Round-Robin — the same scheduler
football-scoreboard/sports.py:_build_weighted_schedulealready uses to rotate its own games. The property that matters is that repeats are spread through the cycle, not clumped; three appearances in a row followed by a long silence would be worse than no boost at all.12 plugins, a favourite's baseball game and an ordinary live hockey game:
18 slots for 12 plugins. Baseball 5×, hockey 3×, everything else once, nothing twice in a row.
Where weight comes from
get_vegas_priority_weight().None:live_weightifhas_live_priority()andhas_live_content()are both true.Because of step 2, existing plugins need no changes — any scoreboard with
live_priorityalready gets extra turns. Step 1 exists for the one thing the core cannot work out: it can see that a game is live, not whose, so only the plugin can report a favourite. That's ledmatrix-plugins#273.Weights clamp to 1–10. A raising hook is caught and logged, and the core then falls back to its own live-content check — so a plugin whose weight calculation is broken keeps the live boost and loses only the favourite distinction. (An earlier draft of this description said such a hook is treated as weight 1; the code and docs deliberately do the more useful thing, and
has_live_priority/has_live_contentare separate methods guarded separately.)Documented
Tests
19 in
test/test_vegas_live_weighting.py: where weight comes from and its precedence, clamping, a raising plugin, an unknown plugin; and for the schedule — untouched when nothing is boosted, untouched when the feature is off, correct slot counts, every plugin still appears (a boost must not starve anything out), repeats spread not clumped (max-gap and no-two-in-a-row), and a favourite outranking another live game.2765 passedon the full suite. The one failure,test_install_lowmem.py::TestDiskBackedTmpdir, is pre-existing onmainand environment-dependent.🤖 Generated with Claude Code
https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Summary by CodeRabbit
New Features
Bug Fixes
Documentation