From afde588c8a386595337e594da6fe29252ca2ca75 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Fri, 4 Jul 2025 00:15:25 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20-=20Replace=20gi.repository=20wi?= =?UTF-8?q?th=20Playerctl=20for=20managing=20players?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/dock/components/music_player.py | 464 ++++++++++++++++-------- modules/dock/components/player.py | 4 +- services/mpris.py | 47 ++- 3 files changed, 362 insertions(+), 153 deletions(-) diff --git a/modules/dock/components/music_player.py b/modules/dock/components/music_player.py index 9c95826..8938770 100644 --- a/modules/dock/components/music_player.py +++ b/modules/dock/components/music_player.py @@ -1,10 +1,12 @@ +import time + from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.circularprogressbar import CircularProgressBar from fabric.widgets.eventbox import EventBox from fabric.widgets.label import Label from fabric.widgets.overlay import Overlay -from gi.repository import Gdk +from gi.repository import Gdk, GLib, Playerctl import config.data as data import utils.icons as icons @@ -113,6 +115,10 @@ class MusicPlayer(Box): self.position = 0.0 self._current_track_info = "Music Player" + # Debouncing for player switching to prevent rapid changes + self._last_player_switch = 0 + self._switch_debounce_time = 1.0 # 1 second minimum between switches + # Create circular progress bar self.progress_bar = CircularProgressBar( name="player-circle", @@ -153,8 +159,6 @@ class MusicPlayer(Box): self.update_visibility() # Set up periodic check for active player - from gi.repository import GLib - self.active_player_check_timeout = GLib.timeout_add_seconds( 2, self.check_active_player ) @@ -164,13 +168,29 @@ class MusicPlayer(Box): def on_destroy(self, widget): """Clean up popup when component is destroyed""" - if hasattr(self, "active_player_check_timeout"): - from gi.repository import GLib + try: + # Remove timeout + if hasattr(self, "active_player_check_timeout"): + GLib.source_remove(self.active_player_check_timeout) - GLib.source_remove(self.active_player_check_timeout) - if self.popup: - self.popup.destroy() - self.popup = None + # Clean up current player service + if hasattr(self, 'current_player_service') and self.current_player_service: + try: + if hasattr(self.current_player_service, 'cleanup'): + self.current_player_service.cleanup() + except Exception as e: + print(f"[DEBUG] Error cleaning up player service on destroy: {e}") + + # Clean up popup + if hasattr(self, 'popup') and self.popup: + try: + self.popup.destroy() + self.popup = None + except Exception as e: + print(f"[DEBUG] Error destroying popup: {e}") + + except Exception as e: + print(f"[DEBUG] Error in on_destroy: {e}") def on_new_player(self, manager, player): """Handle new player detection""" @@ -209,8 +229,6 @@ class MusicPlayer(Box): ): for player_name in self.manager._manager.props.player_names: try: - from gi.repository import Playerctl - candidate_player = Playerctl.Player.new_from_name(player_name) # Check if this player has media content @@ -245,29 +263,49 @@ class MusicPlayer(Box): def get_playing_player(self): """Find a currently playing player from the manager""" try: - if hasattr(self.manager, "_manager") and hasattr( - self.manager._manager, "props" - ): - for player_name in self.manager._manager.props.player_names: - try: - from gi.repository import Playerctl + if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"): + return None - player = Playerctl.Player.new_from_name(player_name) - if ( - hasattr(player, "props") - and hasattr(player.props, "playback_status") - and player.props.playback_status.value_name - == "PLAYERCTL_PLAYBACK_STATUS_PLAYING" - ): - print( - f"[DEBUG] Found playing player: { - player.props.player_name - }" - ) - return player - except Exception as e: - print(f"[DEBUG] Error checking player {player_name}: {e}") + player_names = self.manager._manager.props.player_names + if not player_names: + return None + + for player_name in player_names: + try: + # Validate player_name before creating player + if not player_name or not hasattr(player_name, 'name'): continue + + player = Playerctl.Player.new_from_name(player_name) + + # Comprehensive validation of player object + if not player: + continue + + if not hasattr(player, "props"): + continue + + if not hasattr(player.props, "playback_status"): + continue + + if not hasattr(player.props, "player_name"): + continue + + # Check if player is actually playing + try: + status = player.props.playback_status + if status and hasattr(status, 'value_name'): + if status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING": + print(f"[DEBUG] Found playing player: {player.props.player_name}") + return player + except Exception as e: + print(f"[DEBUG] Error checking playback status for {player_name}: {e}") + continue + + except Exception as e: + print(f"[DEBUG] Error checking player {player_name}: {e}") + continue + except Exception as e: print(f"[DEBUG] Error in get_playing_player: {e}") return None @@ -275,51 +313,61 @@ class MusicPlayer(Box): def check_active_player(self): """Periodically check for the currently playing player and switch if needed""" try: + current_time = time.time() + + # Debounce player switching to prevent rapid changes + if current_time - self._last_player_switch < self._switch_debounce_time: + return True + # Find the currently playing player playing_player = self.get_playing_player() if playing_player: # If we found a playing player and it's different from current, switch to it - if ( - not self.current_player - or self.current_player.props.player_name - != playing_player.props.player_name - ): - print( - f"[DEBUG] Switching to playing player: { - playing_player.props.player_name - }" - ) - self.set_current_player(playing_player) - elif self.current_player: - # If no player is playing, check if current player is paused - if ( - self.current_player.props.playback_status.value_name - != "PLAYERCTL_PLAYBACK_STATUS_PLAYING" - ): - # Current player is paused, see if there's another player available - if hasattr(self.manager, "_manager") and hasattr( - self.manager._manager, "props" - ): - for player_name in self.manager._manager.props.player_names: - try: - from gi.repository import Playerctl + should_switch = False - player = Playerctl.Player.new_from_name(player_name) - if ( - player.props.player_name - != self.current_player.props.player_name - ): - # Switch to a different available player - print( - f"[DEBUG] Switching to available player: { - player.props.player_name - }" - ) - self.set_current_player(player) - break - except: - continue + if not self.current_player: + should_switch = True + else: + try: + # Safely compare player names + if (hasattr(self.current_player, 'props') and + hasattr(self.current_player.props, 'player_name') and + hasattr(playing_player, 'props') and + hasattr(playing_player.props, 'player_name')): + + if self.current_player.props.player_name != playing_player.props.player_name: + should_switch = True + else: + # If we can't safely compare, assume we should switch + should_switch = True + except Exception as e: + print(f"[DEBUG] Error comparing players: {e}") + should_switch = True + + if should_switch: + print(f"[DEBUG] Switching to playing player: {playing_player.props.player_name}") + self.set_current_player(playing_player) + self._last_player_switch = current_time + + elif self.current_player: + # If no player is playing, check if current player is still valid and paused + try: + if (hasattr(self.current_player, 'props') and + hasattr(self.current_player.props, 'playback_status') and + hasattr(self.current_player.props.playback_status, 'value_name')): + + if self.current_player.props.playback_status.value_name != "PLAYERCTL_PLAYBACK_STATUS_PLAYING": + # Current player is paused, see if there's another player available + self._try_switch_to_alternative_player(current_time) + else: + # Current player is invalid, try to find another + self._try_switch_to_alternative_player(current_time) + + except Exception as e: + print(f"[DEBUG] Error checking current player status: {e}") + # Current player seems invalid, try to find another + self._try_switch_to_alternative_player(current_time) # Update visibility based on current state self.update_visibility() @@ -330,63 +378,184 @@ class MusicPlayer(Box): # Return True to continue the timeout return True + def _try_switch_to_alternative_player(self, current_time): + """Try to switch to an alternative player when current is paused/invalid""" + try: + if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"): + return + + player_names = self.manager._manager.props.player_names + if not player_names: + return + + for player_name in player_names: + try: + # Validate player_name + if not player_name or not hasattr(player_name, 'name'): + continue + + player = Playerctl.Player.new_from_name(player_name) + + # Validate player object + if not player or not hasattr(player, 'props') or not hasattr(player.props, 'player_name'): + continue + + # Skip if this is the same as current player + if (self.current_player and + hasattr(self.current_player, 'props') and + hasattr(self.current_player.props, 'player_name') and + player.props.player_name == self.current_player.props.player_name): + continue + + # Switch to a different available player + print(f"[DEBUG] Switching to available player: {player.props.player_name}") + self.set_current_player(player) + self._last_player_switch = current_time + break + + except Exception as e: + print(f"[DEBUG] Error checking alternative player {player_name}: {e}") + continue + + except Exception as e: + print(f"[DEBUG] Error in _try_switch_to_alternative_player: {e}") + def set_current_player(self, player): """Set the current active player""" - print(f"[DEBUG] Setting current player to: {player.props.player_name}") + try: + # Validate player before proceeding + if not player or not hasattr(player, 'props') or not hasattr(player.props, 'player_name'): + print("[DEBUG] Invalid player object, skipping") + return - # Disconnect from previous player - if self.current_player_service: + print(f"[DEBUG] Setting current player to: {player.props.player_name}") + + # Properly cleanup previous player service + if self.current_player_service: + try: + # Use the cleanup method if available + if hasattr(self.current_player_service, 'cleanup'): + self.current_player_service.cleanup() + else: + # Fallback: Stop any running fabricators/timers manually + if hasattr(self.current_player_service, 'pos_fabricator'): + self.current_player_service.pos_fabricator.stop() + + # Disconnect signals with specific error handling + try: + self.current_player_service.disconnect_by_func(self.on_track_position) + except (TypeError, AttributeError) as e: + print(f"[DEBUG] Error disconnecting track_position: {e}") + + try: + self.current_player_service.disconnect_by_func(self.on_play) + except (TypeError, AttributeError) as e: + print(f"[DEBUG] Error disconnecting play: {e}") + + try: + self.current_player_service.disconnect_by_func(self.on_pause) + except (TypeError, AttributeError) as e: + print(f"[DEBUG] Error disconnecting pause: {e}") + + try: + self.current_player_service.disconnect_by_func(self.on_metadata) + except (TypeError, AttributeError) as e: + print(f"[DEBUG] Error disconnecting metadata: {e}") + + except Exception as e: + print(f"[DEBUG] Error during player service cleanup: {e}") + + self.current_player = player + + # Create new player service with error handling try: - self.current_player_service.disconnect_by_func(self.on_track_position) - self.current_player_service.disconnect_by_func(self.on_play) - self.current_player_service.disconnect_by_func(self.on_pause) - self.current_player_service.disconnect_by_func(self.on_metadata) - except: - pass + self.current_player_service = PlayerService(player=player) + except Exception as e: + print(f"[DEBUG] Error creating PlayerService: {e}") + self.current_player_service = None + return - self.current_player = player - self.current_player_service = PlayerService(player=player) + # Connect to player events with error handling + if self.current_player_service: + try: + self.current_player_service.connect("track-position", self.on_track_position) + self.current_player_service.connect("play", self.on_play) + self.current_player_service.connect("pause", self.on_pause) + self.current_player_service.connect("meta-change", self.on_metadata) + except Exception as e: + print(f"[DEBUG] Error connecting to player service signals: {e}") - # Connect to player events - self.current_player_service.connect("track-position", self.on_track_position) - self.current_player_service.connect("play", self.on_play) - self.current_player_service.connect("pause", self.on_pause) - self.current_player_service.connect("meta-change", self.on_metadata) + # Always use generic music icon, not player-specific icons + self.music_label.set_markup(icons.music) - # Always use generic music icon, not player-specific icons - self.music_label.set_markup(icons.music) + # Update initial state + self.update_playback_state() - # Update initial state - self.update_playback_state() + except Exception as e: + print(f"[DEBUG] Critical error in set_current_player: {e}") + # Reset to safe state + self.current_player = None + self.current_player_service = None def on_track_position(self, service, pos, dur): """Handle track position updates""" - self.position = pos - self.duration = dur + try: + # Validate input parameters + if pos is None or dur is None: + return - if dur > 0: - progress = pos / dur - # Use value property instead of set_percentage for CircularProgressBar - self.progress_bar.value = progress + # Ensure pos and dur are numeric + try: + pos = float(pos) + dur = float(dur) + except (ValueError, TypeError): + print(f"[DEBUG] Invalid position/duration values: pos={pos}, dur={dur}") + return - # Update tooltip with timestamp info - pos_min = int(pos // 60) - pos_sec = int(pos % 60) - dur_min = int(dur // 60) - dur_sec = int(dur % 60) - timestamp_text = ( - f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}" - ) + # Validate ranges + if pos < 0 or dur < 0: + return - # Get current track info if available - if hasattr(self, "_current_track_info"): - tooltip = f"{self._current_track_info}\n{timestamp_text}" + self.position = pos + self.duration = dur + + if dur > 0: + progress = min(pos / dur, 1.0) # Ensure progress doesn't exceed 1.0 + + # Safely update progress bar + try: + self.progress_bar.value = progress + except Exception as e: + print(f"[DEBUG] Error updating progress bar: {e}") + + # Update tooltip with timestamp info + try: + pos_min = int(pos // 60) + pos_sec = int(pos % 60) + dur_min = int(dur // 60) + dur_sec = int(dur % 60) + timestamp_text = ( + f"{pos_min:02d}:{pos_sec:02d} / {dur_min:02d}:{dur_sec:02d}" + ) + + # Get current track info if available + if hasattr(self, "_current_track_info") and self._current_track_info: + tooltip = f"{self._current_track_info}\n{timestamp_text}" + else: + tooltip = f"Music Player\n{timestamp_text}" + + self.event_box.set_tooltip_text(tooltip) + except Exception as e: + print(f"[DEBUG] Error updating tooltip: {e}") else: - tooltip = f"Music Player\n{timestamp_text}" - self.event_box.set_tooltip_text(tooltip) - else: - self.progress_bar.value = 0 - self.event_box.set_tooltip_text("Music Player") + try: + self.progress_bar.value = 0 + self.event_box.set_tooltip_text("Music Player") + except Exception as e: + print(f"[DEBUG] Error resetting progress bar: {e}") + + except Exception as e: + print(f"[DEBUG] Error in on_track_position: {e}") def on_play(self, service): """Handle play state""" @@ -500,19 +669,28 @@ class MusicPlayer(Box): def has_running_players(self): """Check if there are any players that actually have media content running""" try: - if hasattr(self.manager, "_manager") and hasattr( - self.manager._manager, "props" - ): - for player_name in self.manager._manager.props.player_names: - try: - from gi.repository import Playerctl + if not hasattr(self.manager, "_manager") or not hasattr(self.manager._manager, "props"): + return False - player = Playerctl.Player.new_from_name(player_name) + player_names = self.manager._manager.props.player_names + if not player_names: + return False - # Check if player has metadata indicating actual media content - if hasattr(player, "props") and hasattr( - player.props, "metadata" - ): + for player_name in player_names: + try: + # Validate player_name before creating player + if not player_name or not hasattr(player_name, 'name'): + continue + + player = Playerctl.Player.new_from_name(player_name) + + # Validate player object + if not player or not hasattr(player, "props"): + continue + + # Check if player has metadata indicating actual media content + if hasattr(player.props, "metadata"): + try: metadata = player.props.metadata if metadata and len(metadata.keys()) > 0: # Check for essential metadata that indicates real media @@ -522,30 +700,26 @@ class MusicPlayer(Box): or "xesam:artist" in keys or "mpris:length" in keys ): - print( - f"[DEBUG] Found player with media content: { - player.props.player_name - }" - ) + print(f"[DEBUG] Found player with media content: {player.props.player_name}") return True + except Exception as e: + print(f"[DEBUG] Error checking metadata for {player_name}: {e}") - # Also check if player is currently playing (even without full metadata) - if ( - hasattr(player, "props") - and hasattr(player.props, "playback_status") - and player.props.playback_status.value_name - == "PLAYERCTL_PLAYBACK_STATUS_PLAYING" - ): - print( - f"[DEBUG] Found playing player: { - player.props.player_name - }" - ) - return True + # Also check if player is currently playing (even without full metadata) + 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"): + print(f"[DEBUG] Found playing player: {player.props.player_name}") + return True + except Exception as e: + print(f"[DEBUG] Error checking playback status for {player_name}: {e}") + + except Exception as e: + print(f"[DEBUG] Error checking player {player_name}: {e}") + continue - except Exception as e: - print(f"[DEBUG] Error checking player {player_name}: {e}") - continue except Exception as e: print(f"[DEBUG] Error in has_running_players: {e}") diff --git a/modules/dock/components/player.py b/modules/dock/components/player.py index 5026a2b..27f45a5 100644 --- a/modules/dock/components/player.py +++ b/modules/dock/components/player.py @@ -1,3 +1,5 @@ +import re + from fabric.core.service import Signal from fabric.widgets.box import Box from fabric.widgets.button import Button @@ -204,8 +206,6 @@ class Player(Box): self.set_style(f"background-image:url('{metadata['mpris:artUrl']}')") elif "xesam:url" in keys and "youtube.com" in metadata["xesam:url"]: # Simple YouTube thumbnail extraction for Firefox - import re - url = metadata["xesam:url"] match = re.search(r"(?:v=|/)([a-zA-Z0-9_-]{11})", url) if match: diff --git a/services/mpris.py b/services/mpris.py index 14a7a0b..4c0f16c 100644 --- a/services/mpris.py +++ b/services/mpris.py @@ -45,6 +45,14 @@ class PlayerService(Service): ) self.poll_progress() + def cleanup(self): + """Clean up resources when service is no longer needed""" + try: + if hasattr(self, 'pos_fabricator') and self.pos_fabricator: + self.pos_fabricator.stop() + except Exception as e: + print(f"[DEBUG] Error during PlayerService cleanup: {e}") + def on_seeked(self, player, position): if self.status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING": self.pos_fabricator.start() @@ -66,12 +74,39 @@ class PlayerService(Service): self.pos_fabricator.stop() def fabricating(self): - pos = self._player.get_position() / 1_000_000 # seconds - # seconds - dur = self._player.props.metadata["mpris:length"] / 1_000_000 - # print(self._player.get_position()) - # print(f"[progress] {pos:.2f}s / {dur:.2f}s") - self.track_position(pos, dur) + try: + # Validate player object + if not self._player or not hasattr(self._player, 'get_position'): + return + + # Get position safely + try: + pos = self._player.get_position() / 1_000_000 # seconds + except Exception as e: + print(f"[DEBUG] Error getting player position: {e}") + return + + # Get duration safely from metadata + try: + if (hasattr(self._player, 'props') and + hasattr(self._player.props, 'metadata') and + self._player.props.metadata and + "mpris:length" in self._player.props.metadata): + + dur = self._player.props.metadata["mpris:length"] / 1_000_000 # seconds + else: + # No duration available, skip this update + return + except Exception as e: + print(f"[DEBUG] Error getting track duration: {e}") + return + + # Validate values before emitting signal + if pos is not None and dur is not None and dur > 0: + self.track_position(pos, dur) + + except Exception as e: + print(f"[DEBUG] Error in fabricating: {e}") def on_play(self, player, status): print("player is playing: {}".format(player.props.player_name))