✨ 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:
5 files changed
+159
-135
No files matched your search
+1
-1
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
Reference in new issue
Block a user