diff --git a/README.md b/README.md index 2d8c0b0..2c527d8 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Environment variables: | Variable | Effect | |---|---| | `HYPERWALL_WEB=1` | Enable web remote on port 8585 (off by default) | +| `HYPERWALL_SERVER_URL` | Per-launch Emby endpoint override; leaves `config.ini` unchanged (use for LAN/public A/B) | | `HYPERWALL_STATS=1` | Enable per-cell playback stats | | `HYPERWALL_HWDEC` | Override hardware decoder (nvdec, d3d11va, etc.) | | `HYPERWALL_VO` | Override video output (gpu-next, gpu) | diff --git a/hyperwall/app.py b/hyperwall/app.py index bd9e395..cfd64ad 100644 --- a/hyperwall/app.py +++ b/hyperwall/app.py @@ -25,7 +25,7 @@ ) from . import __version__, runtime_banner -from .config import HyperwallConfig, ConfigMissingError +from .config import HyperwallConfig, ConfigMissingError, effective_server_url from .constants import ( CONFIG_FILE, LOG_FILE, @@ -275,8 +275,13 @@ def main() -> None: sys.exit(1) # 7. Emby client + server_url = effective_server_url( + cfg.server_url, os.environ.get("HYPERWALL_SERVER_URL") + ) + if server_url != cfg.server_url: + logger.info("Endpoint override active for this launch: %s", server_url) client = EmbyClient( - cfg.server_url, cfg.username, cfg.password, verify_ssl=cfg.verify_ssl, + server_url, cfg.username, cfg.password, verify_ssl=cfg.verify_ssl, backend=resolve_backend(cfg.backend), ) if not cfg.verify_ssl: @@ -289,7 +294,7 @@ def main() -> None: if not client.test_connection(): _show_error_dialog( "Connection Error", - f"Cannot reach Emby server at:\n{cfg.server_url}", + f"Cannot reach Emby server at:\n{server_url}", ) sys.exit(1) diff --git a/hyperwall/config.py b/hyperwall/config.py index 35e7444..0ca5976 100644 --- a/hyperwall/config.py +++ b/hyperwall/config.py @@ -14,6 +14,16 @@ from .constants import CONFIG_FILE +def effective_server_url(configured: str, override: str | None = None) -> str: + """Return a per-launch endpoint override without changing config.ini. + + This supports controlled LAN-vs-public delivery tests while leaving the + user's normal configured endpoint and credentials untouched. + """ + candidate = (override or "").strip() + return candidate or configured + + @dataclass(frozen=True) class HyperwallConfig: """Immutable configuration loaded from config.ini.""" diff --git a/tests/run_all.py b/tests/run_all.py index d06be44..8b07453 100644 --- a/tests/run_all.py +++ b/tests/run_all.py @@ -34,6 +34,7 @@ "test_platform", "test_soak_telemetry", "test_macos_playback_performance", + "test_endpoint_override", ] diff --git a/tests/test_endpoint_override.py b/tests/test_endpoint_override.py new file mode 100644 index 0000000..bc05107 --- /dev/null +++ b/tests/test_endpoint_override.py @@ -0,0 +1,42 @@ +"""Regression contract for ephemeral Hyperwall endpoint overrides. + +A macOS playback A/B must be able to use Greg's LAN Emby endpoint without +rewriting a configured public endpoint or exposing credentials in commands. +""" +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from hyperwall.config import effective_server_url # noqa: E402 + + +def test_endpoint_override_wins_without_mutating_default(): + configured = "https://mb.perseus.observer" + override = "http://10.168.168.29:8096" + assert effective_server_url(configured, override) == override + assert configured == "https://mb.perseus.observer" + + +def test_blank_override_keeps_configured_endpoint(): + assert effective_server_url("http://emby:8096", " ") == "http://emby:8096" + assert effective_server_url("http://emby:8096", None) == "http://emby:8096" + + +def run_all() -> int: + tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] + passed = failed = 0 + for test in tests: + try: + test() + passed += 1 + print(f" PASS {test.__name__}") + except Exception as e: # noqa: BLE001 + failed += 1 + print(f" FAIL {test.__name__}: {e}") + print(f"\n{passed} passed, {failed} failed out of {len(tests)} tests.") + return failed + + +if __name__ == "__main__": + raise SystemExit(1 if run_all() else 0)