mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-05 06:45:09 +00:00
feat(vegas): one declared participation per plugin (scroll | pause | exclude) (#682)
A plugin takes part in Vegas mode in one declared way: 'scroll', 'pause' or 'exclude', resolved from the user's vegas_participation setting, the manifest field, then the legacy hooks, so no plugin changes behaviour. The stream manager decides inclusion and pauses through it; the installed plugins API and the Vegas plugin-order list report it. Deprecates get_supported_vegas_modes, get_vegas_segment_width and vegas_panel_count for removal in 3.9.0, and regenerates docs/DEPRECATIONS_3.8.md to include them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,3 +53,27 @@ def deprecated(removal: str, alternative: Optional[str] = None) -> Callable[[F],
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def warn_deprecated(what: str, removal: str, alternative: Optional[str] = None,
|
||||
once_key: Optional[str] = None) -> bool:
|
||||
"""Warn that ``what`` will be removed in ``removal``, once per process.
|
||||
|
||||
For what ``@deprecated`` cannot decorate: a config key, a manifest field,
|
||||
a value a hook returns. Same message, log line and DeprecationWarning as
|
||||
the decorator. ``once_key`` (default: ``what``) is what "once" counts
|
||||
against, so one deprecated key can warn once for each plugin that sets it.
|
||||
|
||||
Returns whether this call warned.
|
||||
"""
|
||||
message = f"{what} is deprecated and will be removed in LEDMatrix {removal}"
|
||||
if alternative:
|
||||
message += f"; {alternative}"
|
||||
key = once_key or what
|
||||
with _warned_lock:
|
||||
if key in _warned:
|
||||
return False
|
||||
_warned.add(key)
|
||||
logger.warning(message)
|
||||
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
||||
return True
|
||||
|
||||
@@ -13,6 +13,7 @@ from enum import Enum
|
||||
from typing import Dict, Any, Optional, List
|
||||
import os
|
||||
import sys
|
||||
from src.deprecation import deprecated, warn_deprecated
|
||||
from src.logging_config import get_logger
|
||||
|
||||
|
||||
@@ -65,27 +66,178 @@ def _fallback_font_manager() -> Any:
|
||||
|
||||
class VegasDisplayMode(Enum):
|
||||
"""
|
||||
Display mode for Vegas scroll integration.
|
||||
Legacy display mode for Vegas scroll integration.
|
||||
|
||||
Determines how a plugin's content behaves within the continuous scroll:
|
||||
Superseded by :meth:`BasePlugin.get_vegas_participation`. Vegas still
|
||||
reads a plugin's :meth:`BasePlugin.get_vegas_display_mode` to derive its
|
||||
participation when nothing declares one, and only STATIC matters there:
|
||||
|
||||
- SCROLL: Content scrolls continuously within the stream.
|
||||
Best for multi-item plugins like sports scores, odds tickers, news feeds.
|
||||
Plugin provides multiple frames via get_vegas_content().
|
||||
|
||||
- FIXED_SEGMENT: Content is a fixed-width block that scrolls BY with
|
||||
the rest of the content. Best for static info like clock, weather.
|
||||
Plugin provides a single image sized to vegas_panel_count panels.
|
||||
|
||||
- STATIC: Scroll pauses, plugin displays for its duration, then scroll
|
||||
resumes. Best for important alerts or detailed views that need attention.
|
||||
Plugin uses standard display() method during the pause.
|
||||
- STATIC: the scroll pauses for the plugin's turn and its display() draws
|
||||
it full screen -- participation ``'pause'``.
|
||||
- SCROLL and FIXED_SEGMENT: the plugin's content joins the scroll --
|
||||
participation ``'scroll'``. Vegas has never told the two apart: a card's
|
||||
width comes from get_vegas_content() and ``vegas_width_pct``, not from
|
||||
the mode. The distinction is deprecated and goes away in LEDMatrix 3.9.0.
|
||||
"""
|
||||
SCROLL = "scroll"
|
||||
FIXED_SEGMENT = "fixed"
|
||||
STATIC = "static"
|
||||
|
||||
|
||||
#: How a plugin takes part in Vegas mode (BasePlugin.get_vegas_participation):
|
||||
#:
|
||||
#: - ``'scroll'``: its content joins the scrolling strip.
|
||||
#: - ``'pause'``: the scroll stops when the plugin's turn comes round, and its
|
||||
#: display() draws it full screen for its display duration.
|
||||
#: - ``'exclude'``: it is left out of Vegas mode.
|
||||
VEGAS_PARTICIPATION_VALUES = ('scroll', 'pause', 'exclude')
|
||||
|
||||
#: The release that removes get_supported_vegas_modes(),
|
||||
#: get_vegas_segment_width(), the ``vegas_panel_count`` setting and the
|
||||
#: SCROLL / FIXED_SEGMENT distinction. The two @deprecated markers below
|
||||
#: spell it as a literal, because tools that read markers statically (the
|
||||
#: deprecation tests, the plugin API usage scan) cannot follow a name.
|
||||
VEGAS_LEGACY_REMOVAL = "3.9.0"
|
||||
|
||||
_vegas_logger = get_logger(__name__)
|
||||
_vegas_warned: set = set()
|
||||
|
||||
|
||||
def _vegas_warn_once(key: Any, message: str, *args: Any) -> None:
|
||||
"""Log a warning about a plugin's Vegas settings once per process.
|
||||
|
||||
Participation is resolved at every rotation refresh, so a bad value would
|
||||
otherwise log on every one of them.
|
||||
"""
|
||||
if key in _vegas_warned:
|
||||
return
|
||||
_vegas_warned.add(key)
|
||||
_vegas_logger.warning(message, *args)
|
||||
|
||||
|
||||
def vegas_participation_value(value: Any) -> Optional[str]:
|
||||
"""``value`` as one of VEGAS_PARTICIPATION_VALUES, or None if it is not one.
|
||||
|
||||
Case and surrounding whitespace are ignored; anything that is not a string
|
||||
(None, a MagicMock standing in for a plugin in a test) is not a value.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = value.strip().lower()
|
||||
if value in VEGAS_PARTICIPATION_VALUES:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def configured_vegas_participation(plugin_id: str, config: Any) -> Optional[str]:
|
||||
"""The user's ``vegas_participation`` setting in a plugin's config, if valid.
|
||||
|
||||
An unset or empty value is no setting. Anything else that is not a
|
||||
participation is logged once and ignored, so the plugin keeps its own.
|
||||
"""
|
||||
if not isinstance(config, dict):
|
||||
return None
|
||||
raw = config.get('vegas_participation')
|
||||
if raw is None or (isinstance(raw, str) and not raw.strip()):
|
||||
return None
|
||||
value = vegas_participation_value(raw)
|
||||
if value is None:
|
||||
_vegas_warn_once(
|
||||
('config', plugin_id, repr(raw)),
|
||||
"[%s] Invalid vegas_participation %r, expected one of %s; ignoring it",
|
||||
plugin_id, raw, ', '.join(VEGAS_PARTICIPATION_VALUES))
|
||||
return value
|
||||
|
||||
|
||||
def legacy_vegas_participation(plugin: Any) -> str:
|
||||
"""The participation a plugin's pre-3.8 Vegas hooks describe.
|
||||
|
||||
Exactly what Vegas decided from them before participation existed:
|
||||
|
||||
1. get_vegas_display_mode() returning ``VegasDisplayMode.STATIC`` pauses,
|
||||
whatever the content type -- a STATIC plugin whose content type is
|
||||
``'none'`` was still kept in the rotation to pause it.
|
||||
2. Otherwise get_vegas_content_type() returning ``'none'`` excludes.
|
||||
3. Everything else scrolls. SCROLL and FIXED_SEGMENT were never told
|
||||
apart, and neither were content types ``'multi'``, ``'static'`` or any
|
||||
other string.
|
||||
|
||||
Only the enum member counts as STATIC (a plugin returning the string
|
||||
``'static'`` never paused), and a hook that raises or is missing counts as
|
||||
not STATIC and as content type ``'static'``.
|
||||
"""
|
||||
display_mode = None
|
||||
get_mode = getattr(plugin, 'get_vegas_display_mode', None)
|
||||
if get_mode is not None:
|
||||
try:
|
||||
display_mode = get_mode()
|
||||
except Exception:
|
||||
_vegas_logger.debug("get_vegas_display_mode() failed on %s; not pausing",
|
||||
type(plugin).__name__, exc_info=True)
|
||||
if display_mode == VegasDisplayMode.STATIC:
|
||||
return 'pause'
|
||||
|
||||
content_type = 'static'
|
||||
get_type = getattr(plugin, 'get_vegas_content_type', None)
|
||||
if get_type is not None:
|
||||
try:
|
||||
content_type = get_type()
|
||||
except Exception:
|
||||
_vegas_logger.debug("get_vegas_content_type() failed on %s; treating as 'static'",
|
||||
type(plugin).__name__, exc_info=True)
|
||||
if content_type == 'none':
|
||||
return 'exclude'
|
||||
return 'scroll'
|
||||
|
||||
|
||||
def resolve_vegas_participation(plugin: Any, plugin_id: Optional[str] = None) -> str:
|
||||
"""How Vegas mode treats ``plugin``: ``'scroll'``, ``'pause'`` or ``'exclude'``.
|
||||
|
||||
What the core calls, rather than the plugin's own
|
||||
get_vegas_participation(), so the user's setting wins even over a plugin
|
||||
that overrides that method, and so a plugin that is not a BasePlugin (or a
|
||||
test double) still gets the legacy derivation:
|
||||
|
||||
1. the user's ``vegas_participation`` in the plugin's config;
|
||||
2. the plugin's get_vegas_participation(), when it returns a valid value
|
||||
(BasePlugin's reads the manifest's ``vegas_participation``, then
|
||||
derives one from the legacy hooks);
|
||||
3. legacy_vegas_participation().
|
||||
|
||||
Also where the deprecated ``vegas_panel_count`` setting is reported, once
|
||||
per plugin. Never raises.
|
||||
"""
|
||||
pid = plugin_id or getattr(plugin, 'plugin_id', None) or type(plugin).__name__
|
||||
config = getattr(plugin, 'config', None)
|
||||
if isinstance(config, dict) and 'vegas_panel_count' in config:
|
||||
warn_deprecated(
|
||||
f"The vegas_panel_count setting (plugin '{pid}')", VEGAS_LEGACY_REMOVAL,
|
||||
"it has no effect -- use vegas_width_pct to size the plugin's card",
|
||||
once_key=f"vegas_panel_count:{pid}")
|
||||
|
||||
configured = configured_vegas_participation(pid, config)
|
||||
if configured is not None:
|
||||
return configured
|
||||
|
||||
getter = getattr(plugin, 'get_vegas_participation', None)
|
||||
if callable(getter):
|
||||
try:
|
||||
declared = getter()
|
||||
except Exception:
|
||||
_vegas_logger.exception("[%s] get_vegas_participation() failed; "
|
||||
"using its legacy Vegas hooks", pid)
|
||||
declared = None
|
||||
value = vegas_participation_value(declared)
|
||||
if value is not None:
|
||||
return value
|
||||
if isinstance(declared, str):
|
||||
_vegas_warn_once(
|
||||
('declared', pid, declared),
|
||||
"[%s] get_vegas_participation() returned %r, expected one of %s; "
|
||||
"using its legacy Vegas hooks",
|
||||
pid, declared, ', '.join(VEGAS_PARTICIPATION_VALUES))
|
||||
return legacy_vegas_participation(plugin)
|
||||
|
||||
|
||||
class BasePlugin(ABC):
|
||||
"""
|
||||
Base class that all plugins must inherit from.
|
||||
@@ -834,41 +986,97 @@ class BasePlugin(ABC):
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_vegas_participation(self) -> str:
|
||||
"""
|
||||
How this plugin takes part in Vegas mode: ``'scroll'``, ``'pause'`` or
|
||||
``'exclude'``.
|
||||
|
||||
- ``'scroll'``: the plugin's content (get_vegas_content()) joins the
|
||||
scrolling strip.
|
||||
- ``'pause'``: the scroll stops when the plugin's turn comes round, and
|
||||
its display() draws it full screen for get_display_duration().
|
||||
- ``'exclude'``: the plugin is left out of Vegas mode.
|
||||
|
||||
Resolved in this order:
|
||||
|
||||
1. the user's ``vegas_participation`` setting in this plugin's config
|
||||
(the web UI's per-plugin override);
|
||||
2. ``vegas_participation`` in the plugin's manifest.json -- the way a
|
||||
plugin declares its own default;
|
||||
3. derived from the legacy hooks, so a plugin written before this
|
||||
method existed keeps the behaviour it had: get_vegas_display_mode()
|
||||
returning ``VegasDisplayMode.STATIC`` pauses, otherwise
|
||||
get_vegas_content_type() returning ``'none'`` excludes, and
|
||||
everything else scrolls.
|
||||
|
||||
Declare a fixed participation in the manifest rather than overriding
|
||||
this. Override it only when the answer depends on state -- pause only
|
||||
while an alert is live, exclude while there is nothing to show. Vegas
|
||||
applies the user's setting before calling an override, so an override
|
||||
need not check it.
|
||||
|
||||
Returns:
|
||||
One of VEGAS_PARTICIPATION_VALUES.
|
||||
|
||||
Example:
|
||||
def get_vegas_participation(self):
|
||||
return 'pause' if self._alert_is_live() else 'scroll'
|
||||
"""
|
||||
configured = configured_vegas_participation(self.plugin_id, self.config)
|
||||
if configured is not None:
|
||||
return configured
|
||||
manifest_default = self._manifest_vegas_participation()
|
||||
if manifest_default is not None:
|
||||
return manifest_default
|
||||
return legacy_vegas_participation(self)
|
||||
|
||||
def _manifest_vegas_participation(self) -> Optional[str]:
|
||||
"""``vegas_participation`` from this plugin's manifest, if valid."""
|
||||
manifests = getattr(self.plugin_manager, 'plugin_manifests', None)
|
||||
manifest = manifests.get(self.plugin_id) if isinstance(manifests, dict) else None
|
||||
if not isinstance(manifest, dict) or manifest.get('vegas_participation') is None:
|
||||
return None
|
||||
raw = manifest['vegas_participation']
|
||||
value = vegas_participation_value(raw)
|
||||
if value is None:
|
||||
_vegas_warn_once(
|
||||
('manifest', self.plugin_id, repr(raw)),
|
||||
"[%s] manifest vegas_participation %r is not one of %s; ignoring it",
|
||||
self.plugin_id, raw, ', '.join(VEGAS_PARTICIPATION_VALUES))
|
||||
return value
|
||||
|
||||
def get_vegas_content_type(self) -> str:
|
||||
"""
|
||||
Indicate the type of content this plugin provides for Vegas scroll.
|
||||
Legacy: the type of content this plugin provides for Vegas scroll.
|
||||
|
||||
Override this to specify how Vegas mode should treat this plugin's content.
|
||||
Superseded by get_vegas_participation(). Vegas reads it only to derive
|
||||
a participation when neither the user nor the manifest declares one,
|
||||
and only ``'none'`` matters there: it excludes the plugin (unless
|
||||
get_vegas_display_mode() says STATIC). Every other value scrolls.
|
||||
|
||||
Returns:
|
||||
'multi' - Plugin has multiple scrollable items (sports, odds, news)
|
||||
'static' - Plugin is a static block (clock, weather, music)
|
||||
'none' - Plugin should not appear in Vegas scroll mode
|
||||
|
||||
Example:
|
||||
def get_vegas_content_type(self):
|
||||
return 'multi' # We have multiple games to scroll
|
||||
"""
|
||||
return 'static'
|
||||
|
||||
def get_vegas_display_mode(self) -> VegasDisplayMode:
|
||||
"""
|
||||
Get the display mode for Vegas scroll integration.
|
||||
Legacy: the display mode for Vegas scroll integration.
|
||||
|
||||
This method determines how the plugin's content behaves within Vegas mode:
|
||||
- SCROLL: Content scrolls continuously (multi-item plugins)
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by (clock, weather)
|
||||
- STATIC: Pause scroll to display (alerts, detailed views)
|
||||
Superseded by get_vegas_participation(). Vegas reads it only to derive
|
||||
a participation when neither the user nor the manifest declares one,
|
||||
and only STATIC matters there: it pauses the scroll for the plugin's
|
||||
turn. SCROLL and FIXED_SEGMENT both scroll -- Vegas has never told them
|
||||
apart, and the distinction is deprecated (removed in 3.9.0).
|
||||
|
||||
Override to change default behavior. By default, reads from config
|
||||
or maps legacy get_vegas_content_type() for backward compatibility.
|
||||
Reads the plugin's ``vegas_mode`` config value, else maps
|
||||
get_vegas_content_type() ('multi' to SCROLL, anything else to
|
||||
FIXED_SEGMENT).
|
||||
|
||||
Returns:
|
||||
VegasDisplayMode enum value
|
||||
|
||||
Example:
|
||||
def get_vegas_display_mode(self):
|
||||
return VegasDisplayMode.SCROLL
|
||||
"""
|
||||
# Check for explicit config setting first
|
||||
config_mode = self.config.get("vegas_mode")
|
||||
@@ -888,13 +1096,16 @@ class BasePlugin(ABC):
|
||||
return VegasDisplayMode.SCROLL
|
||||
return VegasDisplayMode.FIXED_SEGMENT
|
||||
|
||||
@deprecated("3.9.0",
|
||||
"nothing reads it -- declare vegas_participation in the manifest instead")
|
||||
def get_supported_vegas_modes(self) -> List[VegasDisplayMode]:
|
||||
"""
|
||||
Return list of Vegas display modes this plugin supports.
|
||||
Deprecated: the Vegas display modes this plugin supports.
|
||||
|
||||
Not currently consulted by core: neither Vegas mode nor the web UI
|
||||
calls it. It is kept, and plugins override it, as the declared set of
|
||||
modes a future mode picker would offer.
|
||||
Never consulted by core -- neither Vegas mode nor the web UI calls it
|
||||
-- and removed in LEDMatrix 3.9.0. A plugin's own override keeps
|
||||
working for the plugin itself; calling this base implementation logs a
|
||||
deprecation warning.
|
||||
|
||||
By default:
|
||||
- 'multi' content type plugins support SCROLL and FIXED_SEGMENT
|
||||
@@ -903,11 +1114,6 @@ class BasePlugin(ABC):
|
||||
|
||||
Returns:
|
||||
List of VegasDisplayMode values this plugin can use
|
||||
|
||||
Example:
|
||||
def get_supported_vegas_modes(self):
|
||||
# This plugin only makes sense as a scrolling ticker
|
||||
return [VegasDisplayMode.SCROLL]
|
||||
"""
|
||||
content_type = self.get_vegas_content_type()
|
||||
|
||||
@@ -918,30 +1124,21 @@ class BasePlugin(ABC):
|
||||
else: # 'static'
|
||||
return [VegasDisplayMode.FIXED_SEGMENT, VegasDisplayMode.STATIC]
|
||||
|
||||
@deprecated("3.9.0",
|
||||
"nothing reads it -- Vegas sizes a card from vegas_width_pct "
|
||||
"(see get_vegas_render_width())")
|
||||
def get_vegas_segment_width(self) -> Optional[int]:
|
||||
"""
|
||||
Get the preferred width for this plugin in Vegas FIXED_SEGMENT mode.
|
||||
Deprecated: the number of panels this plugin wanted as a FIXED_SEGMENT.
|
||||
|
||||
Not currently consulted by core: Vegas mode sizes a card from the
|
||||
``vegas_width_pct`` / ``vegas_scroll.render_width_pct`` settings
|
||||
(see get_vegas_render_width()). Kept because plugins override it.
|
||||
|
||||
Returns the number of panels this plugin should occupy when displayed
|
||||
as a fixed segment. The actual pixel width is calculated as:
|
||||
width = panels * single_panel_width
|
||||
|
||||
Where single_panel_width comes from display.hardware.cols in config.
|
||||
|
||||
Override to provide dynamic sizing based on content.
|
||||
Returns None to use the default (1 panel).
|
||||
Never consulted by core: Vegas sizes a card from the
|
||||
``vegas_width_pct`` / ``vegas_scroll.render_width_pct`` settings (see
|
||||
get_vegas_render_width()). Removed, with the ``vegas_panel_count``
|
||||
setting it reads, in LEDMatrix 3.9.0.
|
||||
|
||||
Returns:
|
||||
Number of panels, or None for default (1 panel)
|
||||
|
||||
Example:
|
||||
def get_vegas_segment_width(self):
|
||||
# Clock needs 2 panels to show time clearly
|
||||
return 2
|
||||
``vegas_panel_count`` from config when it is a positive integer,
|
||||
else None
|
||||
"""
|
||||
raw_value = self.config.get("vegas_panel_count", None)
|
||||
if raw_value is None:
|
||||
|
||||
@@ -569,7 +569,8 @@ class PluginManager:
|
||||
#: prefix rule would silently stop validating it.
|
||||
#:
|
||||
#: Read by: ``vegas_mode/plugin_adapter.py`` (``vegas_width_pct``,
|
||||
#: ``vegas_overflow``) and ``base_plugin.py`` (``vegas_max_width_screens``).
|
||||
#: ``vegas_overflow``) and ``base_plugin.py`` (``vegas_max_width_screens``,
|
||||
#: ``vegas_participation``).
|
||||
#:
|
||||
#: The list itself lives with the other core-owned per-plugin properties in
|
||||
#: ``schema_manager.CORE_PLUGIN_PROPERTIES``, which the web save path also
|
||||
|
||||
@@ -127,8 +127,9 @@ CORE_PLUGIN_PROPERTIES: Dict[str, Dict[str, Any]] = {
|
||||
"description": "Enable live priority takeover when plugin has live content"
|
||||
},
|
||||
# Vegas tuning read by vegas_mode/plugin_adapter.py and base_plugin.py.
|
||||
# Left untyped: the adapter validates them itself and ignores a bad
|
||||
# value with a log line, so a stored one must never block a save.
|
||||
# These three are left untyped: the adapter validates them itself and
|
||||
# ignores a bad value with a log line, so a stored one must never block a
|
||||
# save.
|
||||
"vegas_width_pct": {
|
||||
"description": "Vegas mode: width of this plugin's card, as a percentage of the panel"
|
||||
},
|
||||
@@ -138,6 +139,22 @@ CORE_PLUGIN_PROPERTIES: Dict[str, Dict[str, Any]] = {
|
||||
"vegas_max_width_screens": {
|
||||
"description": "Vegas mode: widest this plugin's card may be, in screens"
|
||||
},
|
||||
# Read by resolve_vegas_participation / BasePlugin.get_vegas_participation.
|
||||
# An enum with no default: a default would be written into every plugin's
|
||||
# config and override the participation the plugin itself declares.
|
||||
"vegas_participation": {
|
||||
"type": "string",
|
||||
"enum": ["scroll", "pause", "exclude"],
|
||||
"title": "Vegas participation",
|
||||
"description": (
|
||||
"Vegas mode: how this plugin takes part in the scrolling ticker. "
|
||||
"'scroll' = its content scrolls by with everything else; "
|
||||
"'pause' = the ticker stops for this plugin's turn and shows it "
|
||||
"full screen for its display duration; "
|
||||
"'exclude' = leave it out of Vegas mode. "
|
||||
"Leave unset to use the plugin's own default."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
#: The keys of CORE_PLUGIN_PROPERTIES that are Vegas tuning rather than plugin
|
||||
@@ -145,6 +162,7 @@ CORE_PLUGIN_PROPERTIES: Dict[str, Dict[str, Any]] = {
|
||||
#: PluginManager.CORE_OWNED_CONFIG_KEYS).
|
||||
CORE_VEGAS_TUNING_KEYS = frozenset({
|
||||
'vegas_width_pct', 'vegas_overflow', 'vegas_max_width_screens',
|
||||
'vegas_participation',
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -5,10 +5,12 @@ Main orchestrator for Vegas-style continuous scroll mode. Coordinates between
|
||||
StreamManager, RenderPipeline, and the display system to provide smooth
|
||||
continuous scrolling of all enabled plugin content.
|
||||
|
||||
Supports three display modes per plugin:
|
||||
- SCROLL: Content scrolls continuously within the stream
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by with other content
|
||||
- STATIC: Scroll pauses, plugin displays for its duration, then resumes
|
||||
Each plugin takes part in one of three ways (its Vegas participation, see
|
||||
BasePlugin.get_vegas_participation):
|
||||
- 'scroll': its content scrolls by within the stream
|
||||
- 'pause': the scroll pauses, the plugin displays for its duration, then
|
||||
the scroll resumes
|
||||
- 'exclude': left out
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -1369,29 +1369,6 @@ class PluginAdapter:
|
||||
logger.debug("[%s] Cleared plugin scroll cache", plugin_id)
|
||||
return cleared
|
||||
|
||||
def get_content_type(self, plugin: 'BasePlugin', plugin_id: str) -> str:
|
||||
"""
|
||||
Get the type of content a plugin provides.
|
||||
|
||||
Args:
|
||||
plugin: Plugin instance
|
||||
plugin_id: Plugin identifier
|
||||
|
||||
Returns:
|
||||
'multi' for multiple items, 'static' for single frame, 'none' for excluded
|
||||
"""
|
||||
if hasattr(plugin, 'get_vegas_content_type'):
|
||||
try:
|
||||
return plugin.get_vegas_content_type()
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
logger.exception(
|
||||
"Error calling get_vegas_content_type() on %s",
|
||||
plugin_id
|
||||
)
|
||||
|
||||
# Default to static for plugins without explicit type
|
||||
return 'static'
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up resources."""
|
||||
with self._cache_lock:
|
||||
|
||||
@@ -5,23 +5,25 @@ Manages plugin content streaming with look-ahead buffering. Maintains a queue
|
||||
of plugin content that's ready to be rendered, prefetching 1-2 plugins ahead
|
||||
of the current scroll position.
|
||||
|
||||
Supports three display modes:
|
||||
- SCROLL: Continuous scrolling content
|
||||
- FIXED_SEGMENT: Fixed block that scrolls by
|
||||
- STATIC: Pause scroll to display (marked for coordinator handling)
|
||||
Each plugin takes part in one of three ways (its Vegas participation, see
|
||||
BasePlugin.get_vegas_participation):
|
||||
- 'scroll': its content joins the strip
|
||||
- 'pause': the scroll pauses for its turn (a STATIC segment, marked for the
|
||||
coordinator)
|
||||
- 'exclude': left out of the rotation
|
||||
"""
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional, List, Dict, Any, Deque, Tuple, TYPE_CHECKING, cast
|
||||
from typing import Optional, List, Dict, Any, Deque, Tuple, TYPE_CHECKING
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from PIL import Image
|
||||
|
||||
from src.vegas_mode.config import VegasModeConfig
|
||||
from src.vegas_mode.plugin_adapter import PluginAdapter
|
||||
from src.plugin_system.base_plugin import VegasDisplayMode
|
||||
from src.plugin_system.base_plugin import VegasDisplayMode, resolve_vegas_participation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.plugin_system.plugin_manager import PluginManager
|
||||
@@ -34,11 +36,12 @@ class ContentSegment:
|
||||
"""One plugin's content for a cycle.
|
||||
|
||||
A STATIC segment carries no images: it marks where the coordinator pauses
|
||||
the scroll to show the plugin full-screen.
|
||||
the scroll to show a plugin whose participation is ``'pause'``. Every
|
||||
other segment is SCROLL.
|
||||
"""
|
||||
plugin_id: str
|
||||
images: List[Image.Image]
|
||||
display_mode: VegasDisplayMode = field(default=VegasDisplayMode.FIXED_SEGMENT)
|
||||
display_mode: VegasDisplayMode = field(default=VegasDisplayMode.SCROLL)
|
||||
|
||||
|
||||
class StreamManager:
|
||||
@@ -335,25 +338,14 @@ class StreamManager:
|
||||
logger.debug("[%s] Vegas: skipped (not enabled)", plugin_id)
|
||||
continue
|
||||
|
||||
# Content type 'none' is left out, except for STATIC plugins,
|
||||
# which pause the scroll rather than contributing to it.
|
||||
content_type = self.plugin_adapter.get_content_type(plugin, plugin_id)
|
||||
display_mode = VegasDisplayMode.FIXED_SEGMENT
|
||||
try:
|
||||
display_mode = plugin.get_vegas_display_mode()
|
||||
except Exception:
|
||||
# Plugin error should not abort refresh; use default mode
|
||||
logger.exception(
|
||||
"[%s] (%s) get_vegas_display_mode() failed, using default",
|
||||
plugin_id, plugin.__class__.__name__
|
||||
)
|
||||
|
||||
included = (content_type != 'none'
|
||||
or display_mode == VegasDisplayMode.STATIC)
|
||||
# 'pause' plugins stay in the rotation: they pause the scroll
|
||||
# for their turn rather than contributing to it.
|
||||
participation = resolve_vegas_participation(plugin, plugin_id)
|
||||
included = participation != 'exclude'
|
||||
logger.debug(
|
||||
"[%s] Vegas: %s (content_type=%s, display_mode=%s)",
|
||||
"[%s] Vegas: %s (participation=%s)",
|
||||
plugin_id, "included" if included else "excluded",
|
||||
content_type, display_mode.value
|
||||
participation
|
||||
)
|
||||
if included:
|
||||
available_plugins.append(plugin_id)
|
||||
@@ -580,23 +572,13 @@ class StreamManager:
|
||||
logger.warning("[%s] Plugin not found in plugin_manager.plugins", plugin_id)
|
||||
return None
|
||||
|
||||
display_mode = VegasDisplayMode.FIXED_SEGMENT
|
||||
try:
|
||||
display_mode = plugin.get_vegas_display_mode()
|
||||
except (AttributeError, TypeError) as e:
|
||||
logger.debug(
|
||||
"[%s] get_vegas_display_mode() not available: %s (using FIXED_SEGMENT)",
|
||||
plugin_id, e
|
||||
)
|
||||
|
||||
# For STATIC mode, we create a placeholder segment
|
||||
# The actual content will be displayed by coordinator during pause
|
||||
if display_mode == VegasDisplayMode.STATIC:
|
||||
# Create minimal placeholder - coordinator handles actual display
|
||||
# A 'pause' plugin gets a placeholder segment; the coordinator
|
||||
# draws it with display() when the scroll reaches its turn.
|
||||
if resolve_vegas_participation(plugin, plugin_id) == 'pause':
|
||||
segment = ContentSegment(
|
||||
plugin_id=plugin_id,
|
||||
images=[], # No images needed for static pause
|
||||
display_mode=display_mode
|
||||
display_mode=VegasDisplayMode.STATIC
|
||||
)
|
||||
self.stats['segments_fetched'] += 1
|
||||
logger.debug(
|
||||
@@ -605,7 +587,6 @@ class StreamManager:
|
||||
)
|
||||
return segment
|
||||
|
||||
# Get content via adapter for SCROLL/FIXED_SEGMENT modes
|
||||
images = self.plugin_adapter.get_content(plugin, plugin_id)
|
||||
if not images:
|
||||
# The adapter already warns when every content path failed;
|
||||
@@ -619,13 +600,13 @@ class StreamManager:
|
||||
segment = ContentSegment(
|
||||
plugin_id=plugin_id,
|
||||
images=images,
|
||||
display_mode=display_mode
|
||||
display_mode=VegasDisplayMode.SCROLL
|
||||
)
|
||||
|
||||
self.stats['segments_fetched'] += 1
|
||||
logger.debug(
|
||||
"[%s] Segment: %d image(s), %dpx, mode=%s",
|
||||
plugin_id, len(images), total_width, display_mode.value
|
||||
"[%s] Segment: %d image(s), %dpx",
|
||||
plugin_id, len(images), total_width
|
||||
)
|
||||
return segment
|
||||
|
||||
@@ -693,16 +674,16 @@ class StreamManager:
|
||||
return layout
|
||||
|
||||
def is_static_plugin(self, plugin_id: str) -> bool:
|
||||
"""Whether a loaded plugin asks Vegas to pause for it (STATIC mode)."""
|
||||
"""Whether a loaded plugin asks Vegas to pause for it (participation 'pause').
|
||||
|
||||
Only 'pause' is acted on here. A plugin whose participation has turned
|
||||
to 'exclude' since the rotation was built is still fetched this cycle,
|
||||
as it always was; the next refresh drops it.
|
||||
"""
|
||||
plugin = getattr(self.plugin_manager, 'plugins', {}).get(plugin_id)
|
||||
if plugin is None:
|
||||
return False
|
||||
try:
|
||||
return cast(bool, plugin.get_vegas_display_mode() == VegasDisplayMode.STATIC)
|
||||
except Exception:
|
||||
logger.debug("[%s] get_vegas_display_mode() failed; treating as not STATIC",
|
||||
plugin_id, exc_info=True)
|
||||
return False
|
||||
return resolve_vegas_participation(plugin, plugin_id) == 'pause'
|
||||
|
||||
def take_next_group(
|
||||
self, count: Optional[int] = None, offscreen_only: bool = False
|
||||
|
||||
Reference in New Issue
Block a user