From 392553ee26035abcc3807554bd8bf47fecbd5348 Mon Sep 17 00:00:00 2001 From: James Mackay Date: Thu, 8 Oct 2026 10:52:08 +0100 Subject: [PATCH] =?UTF-8?q?fix(media=5Fplayer):=20channel-title=20fallback?= =?UTF-8?q?=20gated=20to=20the=20channel=20app=20(TIVIMATE=5FPACKAGE=20or?= =?UTF-8?q?=20audio=5Fplayback=20membership)=20=E2=80=94=20launcher=20neve?= =?UTF-8?q?r=20shows=20a=20channel=20number=20(F1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nvidia_shield/media_player.py | 7 ++- tests/test_command_routing.py | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/custom_components/nvidia_shield/media_player.py b/custom_components/nvidia_shield/media_player.py index e0037ec..ee1fdcf 100644 --- a/custom_components/nvidia_shield/media_player.py +++ b/custom_components/nvidia_shield/media_player.py @@ -57,6 +57,7 @@ from .const import ( DOMAIN, PROTOCOL_MEDIA_KEYS, PROTOCOL_REMOTE_KEYS, + TIVIMATE_PACKAGE, ) from .ha_adb_common import PlaybackIntent, running_apps_from_tasks from .jellyfin import fetch_session_media_info @@ -651,7 +652,11 @@ class NvidiaShieldMediaPlayer(MediaPlayerEntity): state = "playing" if info.playing else "paused" # Privacy mode: identity stays the app, never the media title title = None if not self._adb_store_info else info.title - if title is None: + if title is None and self._last_channel and ( + # F1: the channel number belongs to the channel app only — never + # leak it to the launcher or another foreground app. + foreground == TIVIMATE_PACKAGE or foreground in manager.audio_playback + ): title = self._last_channel if title is None and not self._adb_store_info: title = self.app_name or self.app_id or foreground diff --git a/tests/test_command_routing.py b/tests/test_command_routing.py index 9e4b0e1..271264a 100644 --- a/tests/test_command_routing.py +++ b/tests/test_command_routing.py @@ -13,6 +13,7 @@ import entity_stublib import pytest from entity_stublib import ENTITIES_STUB_NAMES, install, load_entity_modules from nsh.connection import ShieldConnectionManager +from nsh.const import TIVIMATE_PACKAGE @pytest.fixture(autouse=True) @@ -381,6 +382,20 @@ class TestAppFallback: assert player.app_id == "com.google.android.youtube.tv" +def _ladder_player(modules, foreground=None, audio=None): + """Standalone player with a foreground (and optional audio hint) set.""" + player, _s, _i = make_player(modules) + manager = player._manager + manager.store.set("awake", True) + if foreground: + manager.store.set("foreground", foreground) + if audio: + manager.store.set("audio_playback", audio) + player._androidtv_entity = "" + player._read = lambda eid: None + return player, manager + + class TestForegroundLadderTier: """P3-3a: the ladder branch between the media-session tier and ON.""" @@ -470,6 +485,38 @@ class TestForegroundLadderTier: assert np == {"state": "idle", "title": "101", "source": "app"} +class TestChannelLeakGuard: + """F1: the channel number never leaks to a non-channel foreground app.""" + + async def test_launcher_never_shows_the_channel_number(self, modules): + player, _m = _ladder_player( + modules, foreground="com.spocky.projengmenu" + ) + player._last_channel = "101" # entered in TiviMate earlier + + np = player._get_app_now_playing() + + assert np is None # the leak is gone + + async def test_channel_app_still_gets_the_channel_title(self, modules): + player, _m = _ladder_player(modules, foreground=TIVIMATE_PACKAGE) + + player._last_channel = "101" + np = player._get_app_now_playing() + + assert np == {"state": "idle", "title": "101", "source": "app"} + + async def test_audio_member_can_carry_the_channel_title(self, modules): + player, manager = _ladder_player( + modules, foreground="ar.tvplayer.tv", audio=["ar.tvplayer.tv"] + ) + + player._last_channel = "205" + np = player._get_app_now_playing() + + assert np["title"] == "205" + + class TestVolumeAndPowerLadder: async def test_volume_readout_from_protocol_push(self, modules): player, served, _intents = make_player(modules)