From 5cc059bb96a15430fad1e5d1c7707eb0066a01d9 Mon Sep 17 00:00:00 2001 From: Hermes Agent <51974392+tcconnally@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:48:50 +0000 Subject: [PATCH] fix(soak): apply initial favorites corpus --- hyperwall/soak_filter.py | 24 ++++++++++++++++++ hyperwall/wall.py | 18 +++++++++++-- launch.sh | 1 + soak_wall.sh | 2 +- tests/run_all.py | 1 + tests/test_soak_filter.py | 49 ++++++++++++++++++++++++++++++++++++ tests/test_soak_telemetry.py | 2 ++ 7 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 hyperwall/soak_filter.py create mode 100644 tests/test_soak_filter.py diff --git a/hyperwall/soak_filter.py b/hyperwall/soak_filter.py new file mode 100644 index 0000000..ee1bfd7 --- /dev/null +++ b/hyperwall/soak_filter.py @@ -0,0 +1,24 @@ +"""Pure helpers for selecting an initial soak corpus.""" +from __future__ import annotations + +from typing import Any + + +def apply_initial_filter( + items: list[dict[str, Any]], + mode: str | None, +) -> tuple[list[dict[str, Any]], str]: + """Return the initial playback pool and its normalized filter mode. + + The soak-only caller uses ``favorites`` to select a stable corpus before + any cell starts. Normal launches pass an empty mode and retain all items. + """ + normalized = str(mode or "").strip().lower() + if normalized != "favorites": + return list(items), "all" + return [ + item + for item in items + if isinstance(item.get("UserData"), dict) + and item["UserData"].get("IsFavorite") is True + ], "favorites" diff --git a/hyperwall/wall.py b/hyperwall/wall.py index e794ac4..0a3bc24 100644 --- a/hyperwall/wall.py +++ b/hyperwall/wall.py @@ -84,6 +84,7 @@ ) from .urls import build_stream_url_for_plan, tag_names from .playlist import PlaylistManager, DEFAULT_GROUP +from .soak_filter import apply_initial_filter logger = logging.getLogger("HyperWall") @@ -709,10 +710,23 @@ def _on_items_loaded(self, items: list[dict[str, Any]]) -> None: len(source_items), ) self.all_items = source_items - self.filtered = source_items[:] + initial_filter = ( + os.environ.get("HYPERWALL_SOAK_FILTER", "") + if os.environ.get("HYPERWALL_SOAK_ACTIVE") == "1" + else "" + ) + self.filtered, self.filter_mode = apply_initial_filter( + source_items, initial_filter + ) self.playlists.set_source(self.filtered, DEFAULT_GROUP) + if self.filter_mode != "all": + logger.info( + "Filter: %s (%d items)", + self.filter_mode.upper(), + len(self.filtered), + ) logger.info("Metadata Index: %d items loaded.", len(source_items)) - if not source_items: + if not self.filtered: logger.warning( "No playable items returned — check config.ini libraries or " "the Emby library response." diff --git a/launch.sh b/launch.sh index 27f4a81..b234490 100755 --- a/launch.sh +++ b/launch.sh @@ -11,6 +11,7 @@ if [ "${HYPERWALL_SOAK_ACTIVE:-0}" != "1" ]; then unset HYPERWALL_SOAK_DWELL_S unset HYPERWALL_SOAK_ACTIONS unset HYPERWALL_SOAK_PROFILE + unset HYPERWALL_SOAK_FILTER unset HYPERWALL_SOAK_REPORT_DIR unset HYPERWALL_SOAK_REPORT_ROOT unset HYPERWALL_NO_RELAUNCH diff --git a/soak_wall.sh b/soak_wall.sh index 12d582a..04b2c42 100755 --- a/soak_wall.sh +++ b/soak_wall.sh @@ -36,7 +36,7 @@ PIDS=() printf 'started_at=%s\n' "$(python3 -c 'from datetime import datetime, timezone; print(datetime.now(timezone.utc).isoformat())')" printf 'host='; sw_vers printf 'hardware='; system_profiler SPHardwareDataType 2>/dev/null || true - printf 'env=HYPERWALL_SOAK_MINUTES=%s HYPERWALL_SOAK_DWELL_S=%s HYPERWALL_SOAK_PROFILE=%s HYPERWALL_HWDEC=%s HYPERWALL_CACHE_BUDGET_MB=%s HYPERWALL_DEMUXER_PER_CELL_MB=%s\n' "$HYPERWALL_SOAK_MINUTES" "$HYPERWALL_SOAK_DWELL_S" "$HYPERWALL_SOAK_PROFILE" "${HYPERWALL_HWDEC:-}" "${HYPERWALL_CACHE_BUDGET_MB:-}" "${HYPERWALL_DEMUXER_PER_CELL_MB:-}" + printf 'env=HYPERWALL_SOAK_MINUTES=%s HYPERWALL_SOAK_DWELL_S=%s HYPERWALL_SOAK_PROFILE=%s HYPERWALL_SOAK_FILTER=%s HYPERWALL_HWDEC=%s HYPERWALL_CACHE_BUDGET_MB=%s HYPERWALL_DEMUXER_PER_CELL_MB=%s\n' "$HYPERWALL_SOAK_MINUTES" "$HYPERWALL_SOAK_DWELL_S" "$HYPERWALL_SOAK_PROFILE" "${HYPERWALL_SOAK_FILTER:-}" "${HYPERWALL_HWDEC:-}" "${HYPERWALL_CACHE_BUDGET_MB:-}" "${HYPERWALL_DEMUXER_PER_CELL_MB:-}" display_probe="$(system_profiler SPDisplaysDataType 2>/dev/null || true)" if printf '%s\n' "$display_probe" | python3 -c ' import sys diff --git a/tests/run_all.py b/tests/run_all.py index ff57034..dc986e2 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -40,6 +40,7 @@ "test_freeze_visibility", "test_platform", "test_soak_telemetry", + "test_soak_filter", "test_frame_pump", "test_frame_pump_integration", "test_frame_pump_telemetry", diff --git a/tests/test_soak_filter.py b/tests/test_soak_filter.py new file mode 100644 index 0000000..34fdf9a --- /dev/null +++ b/tests/test_soak_filter.py @@ -0,0 +1,49 @@ +"""Pure tests for the soak-only initial corpus filter.""" +from __future__ import annotations + +from hyperwall.soak_filter import apply_initial_filter + + +def test_initial_favorites_filter_selects_only_favorites(): + items = [ + {"Id": "a", "UserData": {"IsFavorite": True}}, + {"Id": "b", "UserData": {"IsFavorite": False}}, + {"Id": "c", "UserData": {}}, + ] + + filtered, mode = apply_initial_filter(items, "favorites") + + assert mode == "favorites" + assert [item["Id"] for item in filtered] == ["a"] + assert [item["Id"] for item in items] == ["a", "b", "c"] + + +def test_initial_filter_is_all_by_default_and_for_unknown_modes(): + items = [{"Id": "a"}, {"Id": "b"}] + + for mode in (None, "", "all", "unexpected"): + filtered, actual_mode = apply_initial_filter(items, mode) + assert actual_mode == "all" + assert filtered == items + assert filtered is not items + + +def run_all() -> int: + tests = [ + test_initial_favorites_filter_selects_only_favorites, + test_initial_filter_is_all_by_default_and_for_unknown_modes, + ] + failures = 0 + for test in tests: + try: + test() + print(f" PASS {test.__name__}") + except Exception as exc: + failures += 1 + print(f" FAIL {test.__name__}: {exc}") + print(f"\n{len(tests) - failures} passed, {failures} failed") + return failures + + +if __name__ == "__main__": + raise SystemExit(run_all()) diff --git a/tests/test_soak_telemetry.py b/tests/test_soak_telemetry.py index b501059..228d595 100644 --- a/tests/test_soak_telemetry.py +++ b/tests/test_soak_telemetry.py @@ -213,6 +213,7 @@ def test_macos_soak_launcher_collects_system_telemetry(): for expected in ( "HYPERWALL_SOAK_PROFILE:-audio", "HYPERWALL_SOAK_REPORT_DIR", + "HYPERWALL_SOAK_FILTER", "HYPERWALL_STATS=1", "HYPERWALL_PERFTRACE=1", "powermetrics", @@ -239,6 +240,7 @@ def test_normal_launcher_clears_stale_soak_environment(): assert 'HYPERWALL_SOAK_ACTIVE:-0' in source assert 'unset HYPERWALL_SOAK_MINUTES' in source assert 'unset HYPERWALL_SOAK_REPORT_DIR' in source + assert 'unset HYPERWALL_SOAK_FILTER' in source assert 'unset HYPERWALL_NO_LOG_SETUP' in source