diff --git a/modules/dock/components/music_player.py b/modules/dock/components/music_player.py index d117463..f3a5d6c 100644 --- a/modules/dock/components/music_player.py +++ b/modules/dock/components/music_player.py @@ -171,6 +171,10 @@ class MusicPlayer(Box): self._last_player_switch = 0 self._switch_debounce_time = 1.0 # 1 second minimum between switches + # Visibility debouncing to prevent immediate hiding when player closes + self._visibility_timeout = None + self._hide_delay = 2.0 # 2 seconds delay before hiding when no players + # Create circular progress bar self.progress_bar = CircularProgressBar( name="player-circle", @@ -239,6 +243,11 @@ class MusicPlayer(Box): GLib.source_remove(self._hover_timeout) self._hover_timeout = None + # Remove visibility timeout + if hasattr(self, "_visibility_timeout") and self._visibility_timeout: + GLib.source_remove(self._visibility_timeout) + self._visibility_timeout = None + # Clean up current player service if hasattr(self, 'current_player_service') and self.current_player_service: try: @@ -730,6 +739,20 @@ class MusicPlayer(Box): def has_running_players(self): """Check if there are any players that actually have media content running""" try: + # First check if we have a current player that's still valid + if self.current_player and self.current_player_service: + try: + # Check if current player still exists and has media + if hasattr(self.current_player.props, "metadata"): + metadata = self.current_player.props.metadata + if metadata and len(metadata.keys()) > 0: + keys = metadata.keys() + if ("xesam:title" in keys or "xesam:artist" in keys or "mpris:length" in keys): + return True + except Exception: + pass + + # Check all available players if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"): return False @@ -762,23 +785,30 @@ class MusicPlayer(Box): or "mpris:length" in keys ): return True - except Exception as e: + except Exception: pass # Also check if player is currently playing (even without full metadata) + # But be more strict - only count as "has media" if actually playing if hasattr(player.props, "playback_status"): try: status = player.props.playback_status if (status and hasattr(status, 'value_name') and status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING"): - return True - except Exception as e: + # Double-check that this isn't just a phantom player + # by trying to get position or metadata + try: + player.get_position() # This will fail if player is dead + return True + except Exception: + pass + except Exception: pass - except Exception as e: + except Exception: pass - except Exception as e: + except Exception: pass return False @@ -792,6 +822,31 @@ class MusicPlayer(Box): self.set_visible(False) return - # Show component only if there are players with actual media content + # Check if there are players with actual media content has_media_players = self.has_running_players() - self.set_visible(has_media_players) \ No newline at end of file + + if has_media_players: + # Cancel any pending hide timeout + if self._visibility_timeout: + GLib.source_remove(self._visibility_timeout) + self._visibility_timeout = None + # Show immediately when media is available + self.set_visible(True) + else: + # Don't hide immediately - use a delay to prevent flickering + # when players are restarting or switching + if not self._visibility_timeout: + self._visibility_timeout = GLib.timeout_add_seconds( + int(self._hide_delay), + self._delayed_hide + ) + + def _delayed_hide(self): + """Hide the component after delay if still no media players""" + # Double-check that we still don't have media players + if not self.has_running_players(): + self.set_visible(False) + + # Clear the timeout + self._visibility_timeout = None + return False # Don't repeat the timeout \ No newline at end of file diff --git a/services/mpris.py b/services/mpris.py index 5e54d67..99dd76a 100644 --- a/services/mpris.py +++ b/services/mpris.py @@ -39,7 +39,7 @@ class PlayerService(Service): self.status = self._player.props.playback_status self.pos_fabricator = Fabricator( interval=1000, # 1s - poll_from=lambda f, *_: self._player.get_position(), + poll_from=lambda f, *_: self._safe_get_position(), on_changed=lambda f, *_: self.fabricating(), ) # Add state tracking to prevent repeated operations @@ -47,6 +47,22 @@ class PlayerService(Service): self._cleanup_done = False self.poll_progress() + def _safe_get_position(self): + """Safely get player position with error handling""" + try: + if not self._player or self._cleanup_done: + return 0 + return self._player.get_position() + except Exception: + # Player is no longer available, stop the fabricator + if hasattr(self, 'pos_fabricator') and self.pos_fabricator and self._fabricator_running: + try: + self.pos_fabricator.stop() + self._fabricator_running = False + except Exception: + pass + return 0 + def cleanup(self): """Clean up resources when service is no longer needed""" if self._cleanup_done: @@ -107,6 +123,10 @@ class PlayerService(Service): def fabricating(self): try: + # Check if cleanup has been done + if self._cleanup_done: + return + # Validate player object if not self._player or not hasattr(self._player, 'get_position'): return @@ -115,6 +135,13 @@ class PlayerService(Service): try: pos = self._player.get_position() / 1_000_000 # seconds except Exception: + # Player is no longer available, stop fabricator + if self._fabricator_running: + try: + self.pos_fabricator.stop() + self._fabricator_running = False + except Exception: + pass return # Get duration safely from multiple sources