fix(vegas): carry the new keys through config, and correct two docs

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
This commit is contained in:
ChuckBuilds
2026-08-12 16:59:09 -04:00
co-authored by Claude Opus 5
parent 8c00df2e13
commit 611eef0597
6 changed files with 62 additions and 10 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ JSON is optional.
| `target_fps` | `125` | Target frame rate | | `target_fps` | `125` | Target frame rate |
| `buffer_ahead` | `2` | Number of plugins buffered ahead | | `buffer_ahead` | `2` | Number of plugins buffered ahead |
This table is a subset — `display.vegas_scroll` supports 29 keys in This table is a subset — `display.vegas_scroll` supports 30 keys in
total. See the full list in total. See the full list in
[CONFIG_REFERENCE.md](CONFIG_REFERENCE.md#displayvegas_scroll--continuous-scroll-mode). [CONFIG_REFERENCE.md](CONFIG_REFERENCE.md#displayvegas_scroll--continuous-scroll-mode).
+4 -2
View File
@@ -201,8 +201,10 @@ def get_vegas_priority_weight(self):
The weight is per *plugin*, not per game: a scoreboard showing four live games The weight is per *plugin*, not per game: a scoreboard showing four live games
still occupies one slot at a time and rotates its own games within it. Values still occupies one slot at a time and rotates its own games within it. Values
are clamped to 110 by the caller, and an exception here is caught and are clamped to 110 by the caller. An exception here is caught and logged, and
treated as weight 1 rather than breaking the rotation. the core then falls back to its own live-content check — so a plugin whose
weight calculation is broken still gets `live_weight` for a game that really
is live, rather than being demoted to 1.
Only consulted when the user has set `vegas_scroll.live_in_ticker`. With the Only consulted when the user has set `vegas_scroll.live_in_ticker`. With the
default (`false`) live content preempts Vegas entirely and there is no ticker default (`false`) live content preempts Vegas entirely and there is no ticker
+4
View File
@@ -587,6 +587,10 @@ class BasePlugin(ABC):
within that slot; this controls how often the plugin itself comes within that slot; this controls how often the plugin itself comes
round. round.
Raising is safe: the core logs it and falls back to its own
live-content check, so a broken weight calculation costs the plugin
the favorite distinction but not the live boost.
Returns: Returns:
Slots per cycle (clamped to 1..10 by the caller), or None to Slots per cycle (clamped to 1..10 by the caller), or None to
defer to the core's own live-content weighting. defer to the core's own live-content weighting.
+12
View File
@@ -236,6 +236,9 @@ class VegasModeConfig:
'lead_in_width': self.lead_in_width, 'lead_in_width': self.lead_in_width,
'plugins_per_cycle': self.plugins_per_cycle, 'plugins_per_cycle': self.plugins_per_cycle,
'max_plugin_width_ratio': self.max_plugin_width_ratio, 'max_plugin_width_ratio': self.max_plugin_width_ratio,
'live_in_ticker': self.live_in_ticker,
'live_weight': self.live_weight,
'favorite_live_weight': self.favorite_live_weight,
'overflow_mode': self.overflow_mode, 'overflow_mode': self.overflow_mode,
'plugin_order': self.plugin_order, 'plugin_order': self.plugin_order,
'excluded_plugins': list(self.excluded_plugins), 'excluded_plugins': list(self.excluded_plugins),
@@ -403,6 +406,15 @@ class VegasModeConfig:
if 'enabled' in vegas_config: if 'enabled' in vegas_config:
self.enabled = vegas_config['enabled'] self.enabled = vegas_config['enabled']
if 'live_in_ticker' in vegas_config:
self.live_in_ticker = bool(vegas_config['live_in_ticker'])
# Clamped exactly as from_config does: a weight below 1 would drop the
# plugin from the rotation, and a huge one starves everything else.
if 'live_weight' in vegas_config:
self.live_weight = max(1, min(10, int(vegas_config['live_weight'])))
if 'favorite_live_weight' in vegas_config:
self.favorite_live_weight = max(
1, min(10, int(vegas_config['favorite_live_weight'])))
if 'scroll_speed' in vegas_config: if 'scroll_speed' in vegas_config:
self.scroll_speed = float(vegas_config['scroll_speed']) self.scroll_speed = float(vegas_config['scroll_speed'])
if 'separator_width' in vegas_config: if 'separator_width' in vegas_config:
+5
View File
@@ -441,6 +441,11 @@ class StreamManager:
if declared is not None: if declared is not None:
return max(1, min(10, int(declared))) return max(1, min(10, int(declared)))
except Exception: except Exception:
# Deliberately falls through to the core's own live check rather
# than demoting to 1. The plugin's weight calculation is broken,
# but has_live_priority() and has_live_content() are separate
# methods guarded separately below -- a plugin that genuinely has
# a live game should still get live_weight for it.
logger.exception("[%s] get_vegas_priority_weight() failed", plugin_id) logger.exception("[%s] get_vegas_priority_weight() failed", plugin_id)
try: try:
+36 -7
View File
@@ -23,23 +23,33 @@ from src.vegas_mode.stream_manager import StreamManager
class FakePlugin: class FakePlugin:
def __init__(self, live=False, declared=None, raises=False): """A plugin that can fail in each place independently.
hook_raises and live_raises are separate because they mean different
things: a broken weight calculation should still leave the core's own
live-content check usable, while a plugin that cannot answer whether it is
live at all has nothing left to fall back on.
"""
def __init__(self, live=False, declared=None, raises=False,
hook_raises=False, live_raises=False):
self._live = live self._live = live
self._declared = declared self._declared = declared
self._raises = raises self._hook_raises = hook_raises or raises
self._live_raises = live_raises or raises
self.enabled = True self.enabled = True
def has_live_priority(self): def has_live_priority(self):
if self._raises: if self._live_raises:
raise RuntimeError("plugin blew up") raise RuntimeError("cannot say whether I am live")
return self._live return self._live
def has_live_content(self): def has_live_content(self):
return self._live return self._live
def get_vegas_priority_weight(self): def get_vegas_priority_weight(self):
if self._raises: if self._hook_raises:
raise RuntimeError("plugin blew up") raise RuntimeError("weight calculation blew up")
return self._declared return self._declared
@@ -91,10 +101,29 @@ class TestWeightsComeFromTheRightPlace:
assert sm._plugin_weight('a') == 10 assert sm._plugin_weight('a') == 10
assert sm._plugin_weight('b') == 1 assert sm._plugin_weight('b') == 1
def test_a_plugin_that_raises_does_not_break_the_rotation(self): def test_a_plugin_that_raises_everywhere_weighs_one(self):
sm = _manager({'bad': FakePlugin(raises=True)}) sm = _manager({'bad': FakePlugin(raises=True)})
assert sm._plugin_weight('bad') == 1 assert sm._plugin_weight('bad') == 1
def test_a_broken_hook_still_earns_the_live_boost(self):
# The hook is only how a plugin asks for *more* than live_weight.
# Losing it should cost the favorite distinction, not the live boost:
# has_live_priority/has_live_content are separate and still work.
sm = _manager({'mlb': FakePlugin(live=True, hook_raises=True)},
live_weight=4)
assert sm._plugin_weight('mlb') == 4
def test_a_broken_hook_on_a_quiet_plugin_weighs_one(self):
sm = _manager({'clock': FakePlugin(live=False, hook_raises=True)},
live_weight=4)
assert sm._plugin_weight('clock') == 1
def test_a_plugin_that_cannot_say_whether_it_is_live_weighs_one(self):
# Nothing left to fall back on, so no boost.
sm = _manager({'mlb': FakePlugin(live=True, live_raises=True)},
live_weight=4)
assert sm._plugin_weight('mlb') == 1
def test_an_unknown_plugin_weighs_one(self): def test_an_unknown_plugin_weighs_one(self):
assert _manager({})._plugin_weight('ghost') == 1 assert _manager({})._plugin_weight('ghost') == 1