From 4ff9c49cddf9925f300ef08b5c170a063b8e62b6 Mon Sep 17 00:00:00 2001 From: Meir Miyara <3089864+mase1981@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:43:59 -0600 Subject: [PATCH] Fix BBC Launcher metadata flapping on UK channels When BBC channels display their launcher overlay, the set-top box briefly reports app state with generic "BBC Launcher" metadata, overwriting valid program information. This causes artwork and channel info to flicker. This fix: - Caches valid linear metadata after successful linear/replay processing - Detects launcher app overlays (name contains "launcher" or appstore logo) - Restores cached metadata instead of showing generic launcher info - Clears cache on standby/offline to prevent stale data Fixes issue where BBC channels show correct info for 2-3 seconds then revert to "BBC Launcher" with no artwork. --- lghorizon/lghorizon_device_state_processor.py | 13 ++++ lghorizon/lghorizon_models.py | 59 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/lghorizon/lghorizon_device_state_processor.py b/lghorizon/lghorizon_device_state_processor.py index e1714ca..fa012f7 100644 --- a/lghorizon/lghorizon_device_state_processor.py +++ b/lghorizon/lghorizon_device_state_processor.py @@ -55,6 +55,11 @@ async def process_state( """Process the device state based on the status message.""" device_state.reset() device_state.state = status_message.running_state + if status_message.running_state in ( + LGHorizonRunningState.ONLINE_STANDBY, + LGHorizonRunningState.OFFLINE, + ): + device_state.clear_linear_metadata_cache() async def process_ui_state( self, @@ -121,6 +126,11 @@ async def _process_apps_state( device_state: LGHorizonDeviceState, apps_state: LGHorizonAppsState, ) -> None: + if device_state.is_launcher_app(apps_state.app_name, apps_state.logo_path): + if device_state.restore_linear_metadata(): + device_state.ui_state_type = LGHorizonUIStateType.MAINUI + return + device_state.id = apps_state.id device_state.show_title = apps_state.app_name device_state.image = apps_state.logo_path @@ -171,6 +181,7 @@ async def _process_linear_state( f"{channel.stream_image}{join_param}{str(random.randrange(1000000))}" ) device_state.image = image_url + device_state.cache_linear_metadata() async def _process_reviewbuffer_state( self, @@ -218,6 +229,7 @@ async def _process_reviewbuffer_state( f"{channel.stream_image}{join_param}{str(random.randrange(1000000))}" ) device_state.image = image_url + device_state.cache_linear_metadata() async def _process_replay_state( self, @@ -258,6 +270,7 @@ async def _process_replay_state( device_state.position = int(player_state.relative_position / 1000) # Add random number to url to force refresh device_state.image = await self._get_intent_image_url(replay_event.event_id) + device_state.cache_linear_metadata() async def _process_vod_state( self, diff --git a/lghorizon/lghorizon_models.py b/lghorizon/lghorizon_models.py index 8ecfa0d..78d7231 100644 --- a/lghorizon/lghorizon_models.py +++ b/lghorizon/lghorizon_models.py @@ -931,6 +931,7 @@ class LGHorizonDeviceState: start_time: Optional[int] = None end_time: Optional[int] = None last_position_update: Optional[int] = None + _last_good_linear_metadata: Dict[str, Any] = field(default_factory=dict) @property def paused(self) -> bool: @@ -965,6 +966,64 @@ def reset(self) -> None: self.reset_progress() + def cache_linear_metadata(self) -> None: + """Cache current linear metadata for fallback when app overlays appear.""" + if not self.channel_name or not self.show_title: + return + self._last_good_linear_metadata = { + "channel_id": self.channel_id, + "channel_name": self.channel_name, + "show_title": self.show_title, + "episode_title": self.episode_title, + "season_number": self.season_number, + "episode_number": self.episode_number, + "image": self.image, + "start_time": self.start_time, + "end_time": self.end_time, + "duration": self.duration, + "position": self.position, + "last_position_update": self.last_position_update, + "source_type": self.source_type, + "media_type": self.media_type, + } + + def restore_linear_metadata(self) -> bool: + """Restore cached linear metadata. Returns True if restored.""" + if not self._last_good_linear_metadata: + return False + self.channel_id = self._last_good_linear_metadata.get("channel_id") + self.channel_name = self._last_good_linear_metadata.get("channel_name") + self.show_title = self._last_good_linear_metadata.get("show_title") + self.episode_title = self._last_good_linear_metadata.get("episode_title") + self.season_number = self._last_good_linear_metadata.get("season_number") + self.episode_number = self._last_good_linear_metadata.get("episode_number") + self.image = self._last_good_linear_metadata.get("image") + self.start_time = self._last_good_linear_metadata.get("start_time") + self.end_time = self._last_good_linear_metadata.get("end_time") + self.duration = self._last_good_linear_metadata.get("duration") + self.position = self._last_good_linear_metadata.get("position") + self.last_position_update = self._last_good_linear_metadata.get("last_position_update") + self.source_type = self._last_good_linear_metadata.get("source_type", LGHorizonSourceType.LINEAR) + self.media_type = self._last_good_linear_metadata.get("media_type", LGHorizonMediaType.CHANNEL) + return True + + def clear_linear_metadata_cache(self) -> None: + """Clear the cached linear metadata.""" + self._last_good_linear_metadata = {} + + @staticmethod + def is_launcher_app(app_name: str, logo_path: str) -> bool: + """Check if app state looks like a launcher overlay (e.g., BBC Launcher).""" + if not app_name: + return False + name_lower = app_name.lower() + logo_lower = (logo_path or "").lower() + if "launcher" in name_lower: + return True + if "appstore" in logo_lower: + return True + return False + class LGHorizonEntitlements: """Class to represent entitlements."""