diff --git a/docs/ADVANCED_FEATURES.md b/docs/ADVANCED_FEATURES.md index f01d79e5..b7c266d7 100644 --- a/docs/ADVANCED_FEATURES.md +++ b/docs/ADVANCED_FEATURES.md @@ -64,7 +64,7 @@ JSON is optional. | `target_fps` | `125` | Target frame rate | | `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 [CONFIG_REFERENCE.md](CONFIG_REFERENCE.md#displayvegas_scroll--continuous-scroll-mode). diff --git a/docs/PLUGIN_API_REFERENCE.md b/docs/PLUGIN_API_REFERENCE.md index a8036f27..4045c50b 100644 --- a/docs/PLUGIN_API_REFERENCE.md +++ b/docs/PLUGIN_API_REFERENCE.md @@ -201,8 +201,10 @@ def get_vegas_priority_weight(self): 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 -are clamped to 1–10 by the caller, and an exception here is caught and -treated as weight 1 rather than breaking the rotation. +are clamped to 1–10 by the caller. An exception here is caught and logged, and +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 default (`false`) live content preempts Vegas entirely and there is no ticker diff --git a/src/plugin_system/base_plugin.py b/src/plugin_system/base_plugin.py index bd51f820..dbe14a49 100644 --- a/src/plugin_system/base_plugin.py +++ b/src/plugin_system/base_plugin.py @@ -587,6 +587,10 @@ class BasePlugin(ABC): within that slot; this controls how often the plugin itself comes 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: Slots per cycle (clamped to 1..10 by the caller), or None to defer to the core's own live-content weighting. diff --git a/src/vegas_mode/config.py b/src/vegas_mode/config.py index 415908fb..f1039f5b 100644 --- a/src/vegas_mode/config.py +++ b/src/vegas_mode/config.py @@ -236,6 +236,9 @@ class VegasModeConfig: 'lead_in_width': self.lead_in_width, 'plugins_per_cycle': self.plugins_per_cycle, '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, 'plugin_order': self.plugin_order, 'excluded_plugins': list(self.excluded_plugins), @@ -403,6 +406,15 @@ class VegasModeConfig: if 'enabled' in vegas_config: 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: self.scroll_speed = float(vegas_config['scroll_speed']) if 'separator_width' in vegas_config: diff --git a/src/vegas_mode/stream_manager.py b/src/vegas_mode/stream_manager.py index 87c8e6d0..080aa9ec 100644 --- a/src/vegas_mode/stream_manager.py +++ b/src/vegas_mode/stream_manager.py @@ -441,6 +441,11 @@ class StreamManager: if declared is not None: return max(1, min(10, int(declared))) 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) try: diff --git a/test/test_vegas_live_weighting.py b/test/test_vegas_live_weighting.py index 3c4dac49..73fe4b26 100644 --- a/test/test_vegas_live_weighting.py +++ b/test/test_vegas_live_weighting.py @@ -23,23 +23,33 @@ from src.vegas_mode.stream_manager import StreamManager 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._declared = declared - self._raises = raises + self._hook_raises = hook_raises or raises + self._live_raises = live_raises or raises self.enabled = True def has_live_priority(self): - if self._raises: - raise RuntimeError("plugin blew up") + if self._live_raises: + raise RuntimeError("cannot say whether I am live") return self._live def has_live_content(self): return self._live def get_vegas_priority_weight(self): - if self._raises: - raise RuntimeError("plugin blew up") + if self._hook_raises: + raise RuntimeError("weight calculation blew up") return self._declared @@ -91,10 +101,29 @@ class TestWeightsComeFromTheRightPlace: assert sm._plugin_weight('a') == 10 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)}) 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): assert _manager({})._plugin_weight('ghost') == 1