✨ feat(music_player, player): add Vivaldi and Brave to allowed players, improve error handling when cleaning up player service and connecting signals, update progress bar behavior for unknown duration, use Playerctl.Player type for active_player_changed signal

This commit is contained in:
sankalp committed 2025-07-11 01:21:34 +05:45
1 parent 13290d38ba
commit 6cc77cda73
5 files changed
+159 -135

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ gi.require_version("Gtk", "3.0")
APP_NAME = "modus"
APP_NAME_CAP = "Modus"
ALLOWED_PLAYERS = ["vlc", "cmus", "firefox", "spotify", "chromium"]
ALLOWED_PLAYERS = ["vlc", "cmus", "firefox", "spotify", "chromium", "vivaldi", "brave"]
NOTIF_POS_KEY = "notif_pos"
NOTIF_POS_DEFAULT = "Top"
+21 -21
View File
@@ -1,27 +1,27 @@
$wallpaper = /home/sankalp/Pictures/wallpapers/5.png
$background = 1a110f
$foreground = f1dfd9
$wallpaper = /home/sankalp/Pictures/wallpapers/48.png
$background = 0f1417
$foreground = dee3e6
$primary = ffb59c
$secondary = e7bdb0
$tertiary = d6c68d
$surface = 1a110f
$surface_bright = 423733
$outline = a08d87
$primary = 89d0ed
$secondary = b3cad5
$tertiary = c4c3eb
$surface = 0f1417
$surface_bright = 353a3d
$outline = 8a9296
$error = ff5945
$shadow = 000000
$red = ffb4a2
$green = b7d085
$yellow = dec56e
$blue = cebdfe
$magenta = fcb0d5
$cyan = 84d5c4
$red = ffb2b9
$green = 95d5a7
$yellow = b8cf84
$blue = afc6ff
$magenta = e4b7f3
$cyan = 82d3e2
$white = 82d3e0
$red_dim = ff623c
$green_dim = 89ad42
$yellow_dim = bb9b2b
$blue_dim = 8359fc
$magenta_dim = f84ea1
$cyan_dim = 3eb59c
$red_dim = ff4c5c
$green_dim = 4cb86a
$yellow_dim = 8baa43
$blue_dim = 497dff
$magenta_dim = c360e4
$cyan_dim = 30b5ce
+84 -92
View File
@@ -178,24 +178,22 @@ class MusicPlayer(Box):
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}")
except Exception:
pass
# 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:
pass
except Exception as e:
print(f"[DEBUG] Error in on_destroy: {e}")
except Exception:
pass
def on_new_player(self, manager, player):
"""Handle new player detection"""
print(f"Music dock component: New player detected - {player.props.player_name}")
# If this is our first player, make it current
if not self.current_player:
self.set_current_player(player)
@@ -213,7 +211,6 @@ class MusicPlayer(Box):
def on_player_vanish(self, manager, player):
"""Handle player disappearing"""
print(f"Music dock component: Player vanished - {player.props.player_name}")
# If the current player vanished, find another one
if (
@@ -243,19 +240,10 @@ class MusicPlayer(Box):
or "xesam:artist" in keys
or "mpris:length" in keys
):
print(
f"[DEBUG] Switching to player with media: {
candidate_player.props.player_name
}"
)
pass
self.set_current_player(candidate_player)
break
except Exception as e:
print(
f"[DEBUG] Error checking candidate player {player_name}: {
e
}"
)
except Exception:
continue
self.update_visibility()
@@ -296,18 +284,14 @@ class MusicPlayer(Box):
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
def check_active_player(self):
@@ -342,11 +326,9 @@ class MusicPlayer(Box):
# 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
@@ -365,7 +347,6 @@ class MusicPlayer(Box):
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)
@@ -373,7 +354,6 @@ class MusicPlayer(Box):
self.update_visibility()
except Exception as e:
print(f"[DEBUG] Error in check_active_player: {e}")
# Return True to continue the timeout
return True
@@ -384,7 +364,6 @@ class MusicPlayer(Box):
self.manager.init_all_players()
self.update_visibility()
except Exception as e:
print(f"[DEBUG] Error in delayed music player init: {e}")
return False # Don't repeat this timeout
def _try_switch_to_alternative_player(self, current_time):
@@ -417,27 +396,22 @@ class MusicPlayer(Box):
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"""
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
print(f"[DEBUG] Setting current player to: {player.props.player_name}")
# Properly cleanup previous player service
if self.current_player_service:
@@ -448,60 +422,63 @@ class MusicPlayer(Box):
else:
# Fallback: Stop any running fabricators/timers manually
if hasattr(self.current_player_service, 'pos_fabricator'):
self.current_player_service.pos_fabricator.stop()
try:
self.current_player_service.pos_fabricator.stop()
except Exception:
pass
# Disconnect signals with specific error handling
# Disconnect signals - only if they were actually connected
# Use a more robust disconnection approach
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}")
# Get all signal handlers and disconnect them
if hasattr(self.current_player_service, 'disconnect'):
# Try to disconnect all signals from this service
self.current_player_service.disconnect()
except Exception:
# If bulk disconnect fails, try individual disconnections
pass
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}")
except Exception:
pass
self.current_player = player
self.current_player_service = None
# Create new player service with error handling
# Create new player service with error handling and recursion prevention
try:
self.current_player_service = PlayerService(player=player)
except Exception as e:
print(f"[DEBUG] Error creating PlayerService: {e}")
self.current_player_service = None
return
# Validate player before creating service
if (hasattr(player, 'props') and
hasattr(player.props, 'player_name') and
player.props.player_name):
# 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}")
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:
# If signal connection fails, clean up the service
if hasattr(self.current_player_service, 'cleanup'):
self.current_player_service.cleanup()
self.current_player_service = None
else:
pass
except Exception:
self.current_player_service = None
# 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 only if we have a valid service
if self.current_player_service:
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
@@ -518,7 +495,6 @@ class MusicPlayer(Box):
pos = float(pos)
dur = float(dur)
except (ValueError, TypeError):
print(f"[DEBUG] Invalid position/duration values: pos={pos}, dur={dur}")
return
# Validate ranges
@@ -534,8 +510,8 @@ class MusicPlayer(Box):
# Safely update progress bar
try:
self.progress_bar.value = progress
except Exception as e:
print(f"[DEBUG] Error updating progress bar: {e}")
except Exception:
pass
# Update tooltip with timestamp info
try:
@@ -554,17 +530,39 @@ class MusicPlayer(Box):
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}")
except Exception:
pass
else:
# Duration is 0 or unknown - show position only if available
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}")
if pos > 0:
# Show position even without duration
pos_min = int(pos // 60)
pos_sec = int(pos % 60)
timestamp_text = f"{pos_min:02d}:{pos_sec:02d}"
except Exception as e:
print(f"[DEBUG] Error in on_track_position: {e}")
# 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} (Duration unknown)"
else:
tooltip = f"Music Player\n{timestamp_text} (Duration unknown)"
self.event_box.set_tooltip_text(tooltip)
# Set progress bar to indeterminate state or small progress
# Use a pulsing animation or small fixed progress to show activity
self.progress_bar.value = 0.1 # Small progress to show activity
else:
self.progress_bar.value = 0
if hasattr(self, "_current_track_info") and self._current_track_info:
self.event_box.set_tooltip_text(f"{self._current_track_info} (Duration unknown)")
else:
self.event_box.set_tooltip_text("Music Player")
except Exception:
pass
except Exception:
pass
def on_play(self, service):
"""Handle play state"""
@@ -709,10 +707,8 @@ 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}")
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, "playback_status"):
@@ -720,17 +716,13 @@ class MusicPlayer(Box):
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 in has_running_players: {e}")
return False
@@ -745,4 +737,4 @@ class MusicPlayer(Box):
# Show component only if there are players with actual media content
has_media_players = self.has_running_players()
self.set_visible(has_media_players)
self.set_visible(has_media_players)
+11 -2
View File
@@ -1,5 +1,6 @@
import re
from gi.repository import Playerctl
from fabric.core.service import Signal
from fabric.widgets.box import Box
from fabric.widgets.button import Button
@@ -174,6 +175,14 @@ class Player(Box):
self.duration = dur
if dur > 0:
self.wiggly.update_value_from_signal(pos / dur)
else:
# Duration is 0 or unknown - show some activity but don't update progress
# Keep the current progress or set to a small value to indicate activity
if pos > 0:
# Show minimal progress to indicate playback is active
self.wiggly.update_value_from_signal(0.1)
else:
self.wiggly.update_value_from_signal(0.0)
def on_seek(self, sender, ratio):
pos = ratio * self.duration # duration in seconds
@@ -314,7 +323,7 @@ class Player(Box):
class PlayerContainer(Box):
@Signal
def active_player_changed(self, player) -> None: ...
def active_player_changed(self, player: Playerctl.Player) -> None: ...
def __init__(self, **kwargs):
super().__init__(name="player-container", orientation="v", **kwargs)
@@ -431,4 +440,4 @@ class PlayerContainer(Box):
if btn.get_name() == curr.get_name():
btn.add_style_class("active")
else:
btn.remove_style_class("active")
btn.remove_style_class("active")
+42 -19
View File
@@ -82,31 +82,54 @@ class PlayerService(Service):
# 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}")
except Exception:
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):
# Get duration safely from multiple sources
dur = 0.0 # Default to 0
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
if (hasattr(self._player, 'props') and
hasattr(self._player.props, 'metadata') and
self._player.props.metadata):
metadata = self._player.props.metadata
# Try mpris:length first (most common)
try:
raw_dur = metadata["mpris:length"]
if raw_dur and raw_dur > 0:
dur = float(raw_dur) / 1_000_000 # seconds
except (KeyError, Exception):
pass
# Fallback to xesam:duration (alternative field)
if dur <= 0:
try:
raw_dur = metadata["xesam:duration"]
if raw_dur and raw_dur > 0:
dur = float(raw_dur) / 1_000_000 # seconds
except (KeyError, Exception):
pass
# Try to get duration from player directly (some players support this)
if dur <= 0:
try:
if hasattr(self._player, 'get_metadata'):
player_metadata = self._player.get_metadata()
if player_metadata:
raw_dur = player_metadata["mpris:length"]
if raw_dur and raw_dur > 0:
dur = float(raw_dur) / 1_000_000
except (KeyError, Exception):
pass
# Validate values before emitting signal
if pos is not None and dur is not None and dur > 0:
# Allow dur=0 for position-only tracking
if pos is not None and dur is not None and pos >= 0 and dur >= 0:
self.track_position(pos, dur)
except Exception as e:
print(f"[DEBUG] Error in fabricating: {e}")
except Exception:
pass
def on_play(self, player, status):
print("player is playing: {}".format(player.props.player_name))
@@ -173,4 +196,4 @@ class PlayerManager(Service):
def _on_player_vanished(self, sender, player, manager):
print("player has exited: {}".format(player.props.player_name))
print(type(player))
self.player_vanish(player)
self.player_vanish(player)