Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion reeln_google_plugin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

__version__ = "0.10.0"
__version__ = "0.11.0"

from reeln_google_plugin.plugin import GooglePlugin

Expand Down
29 changes: 26 additions & 3 deletions reeln_google_plugin/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
95 changes: 93 additions & 2 deletions tests/unit/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,40 @@ 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()
assert plugin.api_version == 1

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:
Expand Down Expand Up @@ -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."""
Expand Down
Loading