diff --git a/docs/building_plugins.md b/docs/building_plugins.md
index 19231adcc..40e652fb9 100644
--- a/docs/building_plugins.md
+++ b/docs/building_plugins.md
@@ -43,6 +43,20 @@ This guide walks you through the process of creating a new plugin for InkyPi.
# update value for next refresh
settings["index"] = settings["index"] + 1
```
+- (Optional) If your plugin should sometimes skip playlist display, implement `skip_display_condition`.
+ - Return `None` to proceed with normal display.
+ - Return a string to skip this playlist cycle. The string is shown as the reason in the generated plugin preview image.
+ - This is intended for plugins that have valid periods with nothing to show, such as a sports scoreboard during the offseason.
+ - If this method fetches data and returns `None`, cache that data in `settings` using a plugin-private, JSON-serializable key so `generate_image` can reuse it instead of making the same external request again.
+ ```python
+ def skip_display_condition(self, settings, device_config, current_dt):
+ games = fetch_games(settings, current_dt)
+ if not games:
+ return "No games to display"
+
+ settings["_scoreboard_games_cache"] = games
+ return None
+ ```
### 3. Create a Settings Template (Optional)
@@ -185,4 +199,3 @@ Your repository must include:
See [InkyPi-Plugin-Template](https://github.com/fatihak/InkyPi-Plugin-Template) for a sample template of a third party plugin.
Once you're done, feel free to add your plugin to the [3rd Party Plugin List](https://github.com/fatihak/InkyPi/wiki/3rd-Party-Plugins) and share it in the [🙌 Show and Tell Discussion Board](https://github.com/fatihak/InkyPi/discussions/categories/show-and-tell).
-
diff --git a/src/plugins/base_plugin/base_plugin.py b/src/plugins/base_plugin/base_plugin.py
index 6a0ec2aad..4b349984e 100644
--- a/src/plugins/base_plugin/base_plugin.py
+++ b/src/plugins/base_plugin/base_plugin.py
@@ -54,6 +54,17 @@ def __init__(self, config, **dependencies):
def generate_image(self, settings, device_config):
raise NotImplementedError("generate_image must be implemented by subclasses")
+ # Optional playlist self-skip hook. Most plugins should not implement this.
+ # Implement it only when a plugin might have valid periods with nothing to
+ # display, such as a sports scoreboard during the offseason. Return None to
+ # proceed with normal display, or return a human-readable string explaining
+ # why the plugin should be skipped for this playlist cycle. If this method
+ # fetches data and then returns None, cache that data in settings using a
+ # plugin-private, JSON-serializable key so generate_image can reuse it
+ # instead of making the same external request again.
+ def skip_display_condition(self, settings, device_config, current_dt):
+ return None
+
def cleanup(self, settings):
"""Optional cleanup method that plugins can override to delete associated resources.
diff --git a/src/plugins/screenshot/screenshot.py b/src/plugins/screenshot/screenshot.py
index 0c8fedfbb..c6527bb1c 100644
--- a/src/plugins/screenshot/screenshot.py
+++ b/src/plugins/screenshot/screenshot.py
@@ -1,26 +1,92 @@
from plugins.base_plugin.base_plugin import BasePlugin
-from PIL import Image
from utils.image_utils import take_screenshot
import logging
logger = logging.getLogger(__name__)
class Screenshot(BasePlugin):
+ DEFAULT_RENDER_WAIT_MS = None
+
+ def __init__(self, config, **dependencies):
+ super().__init__(config, **dependencies)
+ self._captured_image_cache = {}
+
def generate_image(self, settings, device_config):
+ cache_key = self._get_cache_key(settings, device_config)
+ image = self._captured_image_cache.pop(cache_key, None)
+ if image:
+ return image
+
+ image = self._capture_screenshot(settings, device_config)
+
+ if not image:
+ raise RuntimeError("Failed to take screenshot, please check logs.")
+
+ return image
+
+ def skip_display_condition(self, settings, device_config, current_dt):
+ skip_if_blank = settings.get('skipIfBlank')
+ # Default to True if not specified, matching the UI default
+ if skip_if_blank is not None and skip_if_blank not in (True, 'true'):
+ return None
+
+ image = self._capture_screenshot(settings, device_config)
+ if not image:
+ raise RuntimeError("Failed to take screenshot, please check logs.")
+
+ if self._is_single_color(image):
+ return "Screenshot is blank"
- url = settings.get('url')
+ self._captured_image_cache[self._get_cache_key(settings, device_config)] = image
+ return None
+
+ def _capture_screenshot(self, settings, device_config):
+ url = self._get_url(settings)
if not url:
raise RuntimeError("URL is required.")
+ dimensions = self._get_screenshot_dimensions(device_config)
+ virtual_time_budget_ms = self._get_virtual_time_budget_ms(settings)
+
+ logger.info(f"Taking screenshot of url: {url}")
+
+ return take_screenshot(url, dimensions, timeout_ms=40000, virtual_time_budget_ms=virtual_time_budget_ms)
+
+ def _get_url(self, settings):
+ return settings.get('url')
+
+ def _get_screenshot_dimensions(self, device_config):
dimensions = device_config.get_resolution()
if device_config.get_config("orientation") == "vertical":
dimensions = dimensions[::-1]
+ return dimensions
- logger.info(f"Taking screenshot of url: {url}")
+ def _get_virtual_time_budget_ms(self, settings):
+ render_wait_ms = settings.get('renderWaitMs')
+ if render_wait_ms in (None, ''):
+ render_wait_ms = self.DEFAULT_RENDER_WAIT_MS
- image = take_screenshot(url, dimensions, timeout_ms=40000)
+ virtual_time_budget_ms = None
+ if render_wait_ms:
+ try:
+ virtual_time_budget_ms = int(render_wait_ms)
+ except (TypeError, ValueError):
+ raise RuntimeError("Render wait must be a whole number of milliseconds.")
- if not image:
- raise RuntimeError("Failed to take screenshot, please check logs.")
+ if virtual_time_budget_ms < 0:
+ raise RuntimeError("Render wait must be zero or greater.")
+ if virtual_time_budget_ms == 0:
+ virtual_time_budget_ms = None
+
+ return virtual_time_budget_ms
+
+ def _get_cache_key(self, settings, device_config):
+ return (
+ id(settings),
+ self._get_url(settings),
+ settings.get('renderWaitMs'),
+ self._get_screenshot_dimensions(device_config),
+ )
- return image
\ No newline at end of file
+ def _is_single_color(self, image):
+ return len(image.convert("RGBA").getcolors(maxcolors=2) or []) == 1
diff --git a/src/plugins/screenshot/settings.html b/src/plugins/screenshot/settings.html
index 60f0d0313..5da03cb77 100644
--- a/src/plugins/screenshot/settings.html
+++ b/src/plugins/screenshot/settings.html
@@ -3,6 +3,17 @@
+
+
+
+ Optional. Allows pages with delayed JavaScript rendering to finish before the screenshot is captured. Use 0 to disable.
+
+
+
+
+ Skip if blank: Automatically skip display if the screenshot is a single color.
+
+
Warning: Do not run with untrusted URLs, as it may pose a security risk. This may also fail if the website takes too long to load.