diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3a050..2f7cc97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/). +## [0.11.0] - 2026-04-03 + +### Added + +- `get_input_schema()` — declares `thumbnail_image` input for `game_init` when `create_livestream` is enabled +- Reads thumbnail from `plugin_inputs` (new reeln-cli input contribution system) with fallback to `game_info.thumbnail` + +### Changed + +- `min_reeln_version` bumped to `0.0.37` (requires plugin input contribution support) + ## [0.10.0] - 2026-03-25 ### Changed diff --git a/reeln_google_plugin/__init__.py b/reeln_google_plugin/__init__.py index c9b54da..e7a032a 100644 --- a/reeln_google_plugin/__init__.py +++ b/reeln_google_plugin/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -__version__ = "0.10.0" +__version__ = "0.11.0" from reeln_google_plugin.plugin import GooglePlugin diff --git a/reeln_google_plugin/plugin.py b/reeln_google_plugin/plugin.py index fb086e3..9c6322e 100644 --- a/reeln_google_plugin/plugin.py +++ b/reeln_google_plugin/plugin.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from typing import Any +from reeln.models.plugin_input import InputField, PluginInputSchema from reeln.models.plugin_schema import ConfigField, PluginConfigSchema from reeln.plugins.hooks import Hook, HookContext from reeln.plugins.registry import HookRegistry @@ -26,7 +27,7 @@ class GooglePlugin: """ name: str = "google" - version: str = "0.10.0" + version: str = "0.11.0" api_version: int = 1 config_schema: PluginConfigSchema = PluginConfigSchema( @@ -98,7 +99,24 @@ def __init__(self, config: dict[str, Any] | None = None) -> None: self._youtube: Any = None self._playlist_id: str | None = None - min_reeln_version: str = "0.0.31" + min_reeln_version: str = "0.0.37" + + def get_input_schema(self) -> PluginInputSchema: + """Return input contributions based on enabled feature flags.""" + fields: list[InputField] = [] + if self._config.get("create_livestream", False): + fields.append( + InputField( + id="thumbnail_image", + label="Thumbnail Image", + field_type="file", + command="game_init", + plugin_name="google", + required=False, + description="Thumbnail image for YouTube livestream", + ) + ) + return PluginInputSchema(fields=tuple(fields)) def register(self, registry: HookRegistry) -> None: """Register hook handlers with the reeln plugin registry.""" @@ -168,7 +186,12 @@ def on_game_init(self, context: HookContext) -> None: from pathlib import Path description = getattr(game_info, "description", "") - thumbnail_str = getattr(game_info, "thumbnail", "") + + # Prefer plugin_inputs (new input contribution system) over game_info.thumbnail + plugin_inputs = context.data.get("plugin_inputs", {}) + thumbnail_str = plugin_inputs.get("thumbnail_image", "") if isinstance(plugin_inputs, dict) else "" + if not thumbnail_str: + thumbnail_str = getattr(game_info, "thumbnail", "") thumbnail_path = Path(thumbnail_str) if thumbnail_str else None scheduled_start = self._build_scheduled_start(game_info) diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index d5104b5..7db0630 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -7,7 +7,7 @@ def test_version_is_string() -> None: from reeln_google_plugin import __version__ assert isinstance(__version__, str) - assert __version__ == "0.10.0" + assert __version__ # non-empty def test_google_plugin_exported() -> None: diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 72fbc04..1dd856d 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -27,7 +27,9 @@ def test_name(self) -> None: def test_version(self) -> None: plugin = GooglePlugin() - assert plugin.version == "0.10.0" + from reeln_google_plugin import __version__ + + assert plugin.version == __version__ def test_api_version(self) -> None: plugin = GooglePlugin() @@ -35,7 +37,30 @@ def test_api_version(self) -> None: def test_min_reeln_version(self) -> None: plugin = GooglePlugin() - assert plugin.min_reeln_version == "0.0.31" + assert plugin.min_reeln_version # non-empty + + +class TestGooglePluginInputSchema: + def test_no_inputs_when_disabled(self) -> None: + plugin = GooglePlugin() + schema = plugin.get_input_schema() + assert schema.fields == () + + def test_no_inputs_when_create_livestream_false(self) -> None: + plugin = GooglePlugin({"create_livestream": False}) + schema = plugin.get_input_schema() + assert schema.fields == () + + def test_thumbnail_input_when_create_livestream_enabled(self) -> None: + plugin = GooglePlugin({"create_livestream": True}) + schema = plugin.get_input_schema() + assert len(schema.fields) == 1 + field = schema.fields[0] + assert field.id == "thumbnail_image" + assert field.field_type == "file" + assert field.command == "game_init" + assert field.plugin_name == "google" + assert field.required is False class TestGooglePluginConfigSchema: @@ -806,6 +831,72 @@ def test_full_lifecycle(self, plugin_config: dict[str, Any]) -> None: assert context.shared["livestreams"]["google"] == "https://youtube.com/live/lifecycle-test" +class TestOnGameInitThumbnail: + def test_thumbnail_from_plugin_inputs(self, plugin_config: dict[str, Any]) -> None: + """plugin_inputs thumbnail_image takes precedence over game_info.thumbnail.""" + plugin = GooglePlugin(plugin_config) + game_info = FakeGameInfo(thumbnail="/old/thumb.jpg") + context = HookContext( + hook=Hook.ON_GAME_INIT, + data={ + "game_info": game_info, + "plugin_inputs": {"thumbnail_image": "/new/thumb.png"}, + }, + ) + + with ( + patch("reeln_google_plugin.plugin.auth.get_credentials", return_value=MagicMock()), + patch("reeln_google_plugin.plugin.auth.build_youtube_service", return_value=MagicMock()), + patch( + "reeln_google_plugin.plugin.livestream.create_livestream", + return_value="https://youtube.com/live/test", + ) as mock_create, + ): + plugin.on_game_init(context) + + # Should use plugin_inputs, not game_info.thumbnail + call_kwargs = mock_create.call_args[1] + assert call_kwargs["thumbnail_path"] == Path("/new/thumb.png") + + def test_thumbnail_fallback_to_game_info(self, plugin_config: dict[str, Any]) -> None: + """Falls back to game_info.thumbnail when no plugin_inputs.""" + plugin = GooglePlugin(plugin_config) + game_info = FakeGameInfo(thumbnail="/fallback/thumb.jpg") + context = HookContext(hook=Hook.ON_GAME_INIT, data={"game_info": game_info}) + + with ( + patch("reeln_google_plugin.plugin.auth.get_credentials", return_value=MagicMock()), + patch("reeln_google_plugin.plugin.auth.build_youtube_service", return_value=MagicMock()), + patch( + "reeln_google_plugin.plugin.livestream.create_livestream", + return_value="https://youtube.com/live/test", + ) as mock_create, + ): + plugin.on_game_init(context) + + call_kwargs = mock_create.call_args[1] + assert call_kwargs["thumbnail_path"] == Path("/fallback/thumb.jpg") + + def test_thumbnail_none_when_empty(self, plugin_config: dict[str, Any]) -> None: + """No thumbnail when neither plugin_inputs nor game_info provides one.""" + plugin = GooglePlugin(plugin_config) + game_info = FakeGameInfo() + context = HookContext(hook=Hook.ON_GAME_INIT, data={"game_info": game_info}) + + with ( + patch("reeln_google_plugin.plugin.auth.get_credentials", return_value=MagicMock()), + patch("reeln_google_plugin.plugin.auth.build_youtube_service", return_value=MagicMock()), + patch( + "reeln_google_plugin.plugin.livestream.create_livestream", + return_value="https://youtube.com/live/test", + ) as mock_create, + ): + plugin.on_game_init(context) + + call_kwargs = mock_create.call_args[1] + assert call_kwargs["thumbnail_path"] is None + + class TestOnGameInitPlaylist: def test_disabled_by_default(self) -> None: """When manage_playlists is not set (default False), no playlist created."""