♻️ refactor(dock/components/music_player): update MusicPlayer to use PlayerContainer and handle active player changes

This commit is contained in:
sankalp committed 2025-06-30 01:07:50 +05:45
1 parent 0b652ff87a
commit 994b44f515
6 files changed
+242 -388

No files matched your search

+160 -9
View File
@@ -10,13 +10,13 @@ import config.data as data
import utils.icons as icons
from services.mpris import PlayerManager, PlayerService
from utils.wayland import WaylandWindow as Window
from player import PlayerContainer
from modules.dock.components.player import PlayerContainer
class MusicPlayerPopup(Window):
"""Popup window that shows the full music player interface"""
def __init__(self, **kwargs):
def __init__(self, dock_component=None, **kwargs):
# Get dock position for proper anchoring
dock_position = data.DOCK_POSITION
popup_anchor = self._get_popup_anchor(dock_position)
@@ -34,14 +34,24 @@ class MusicPlayerPopup(Window):
**kwargs,
)
self.dock_component = dock_component
self.player_container = PlayerContainer()
self.add(self.player_container)
# Connect player container events to sync with dock
if self.dock_component:
self.player_container.connect("active-player-changed", self.on_active_player_changed)
# Connect to escape key to close
self.connect("key-press-event", self.on_key_press)
self.connect("button-press-event", self.on_button_press)
self.set_can_focus(True)
def on_active_player_changed(self, container, player):
"""Handle active player change from player container"""
if self.dock_component and player:
self.dock_component.set_current_player(player)
def on_key_press(self, widget, event):
"""Handle key press events"""
if event.keyval == Gdk.KEY_Escape:
@@ -140,11 +150,18 @@ class MusicPlayer(Box):
# Set initial visibility
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)
# Connect to destroy signal for cleanup
self.connect("destroy", self.on_destroy)
def on_destroy(self, widget):
"""Clean up popup when component is destroyed"""
if hasattr(self, 'active_player_check_timeout'):
from gi.repository import GLib
GLib.source_remove(self.active_player_check_timeout)
if self.popup:
self.popup.destroy()
self.popup = None
@@ -156,6 +173,11 @@ class MusicPlayer(Box):
# If this is our first player, make it current
if not self.current_player:
self.set_current_player(player)
else:
# If the new player is playing, switch to it
if (player.props.playback_status.value_name == "PLAYERCTL_PLAYBACK_STATUS_PLAYING" and
self.current_player.props.playback_status.value_name != "PLAYERCTL_PLAYBACK_STATUS_PLAYING"):
self.set_current_player(player)
self.update_visibility()
@@ -171,13 +193,93 @@ class MusicPlayer(Box):
self.current_player = None
self.current_player_service = None
# Try to find another active player
# This is a simple approach - in a real implementation you might want more sophisticated logic
# Try to find another player with media content
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
candidate_player = Playerctl.Player.new_from_name(player_name)
# Check if this player has media content
if (hasattr(candidate_player, 'props') and
hasattr(candidate_player.props, 'metadata')):
metadata = candidate_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):
print(f"[DEBUG] Switching to player with media: {candidate_player.props.player_name}")
self.set_current_player(candidate_player)
break
except Exception as e:
print(f"[DEBUG] Error checking candidate player {player_name}: {e}")
continue
self.update_visibility()
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
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}")
continue
except Exception as e:
print(f"[DEBUG] Error in get_playing_player: {e}")
return None
def check_active_player(self):
"""Periodically check for the currently playing player and switch if needed"""
try:
# 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
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
# Update visibility based on current state
self.update_visibility()
except Exception as e:
print(f"[DEBUG] Error in check_active_player: {e}")
# Return True to continue the timeout
return True
def set_current_player(self, player):
"""Set the current active player"""
print(f"[DEBUG] Setting current player to: {player.props.player_name}")
# Disconnect from previous player
if self.current_player_service:
try:
@@ -238,6 +340,11 @@ class MusicPlayer(Box):
if hasattr(self, '_current_track_info'):
self.event_box.set_tooltip_text(f"{self._current_track_info} (Playing)")
# Trigger immediate check for active player when play state changes
self.check_active_player()
# Update visibility when playback state changes
self.update_visibility()
def on_pause(self, service):
"""Handle pause state"""
# Always use generic music icon when paused
@@ -247,6 +354,11 @@ class MusicPlayer(Box):
if hasattr(self, '_current_track_info'):
self.event_box.set_tooltip_text(f"{self._current_track_info} (Paused)")
# Trigger immediate check for active player when pause state changes
self.check_active_player()
# Update visibility when playback state changes
self.update_visibility()
def on_metadata(self, service, metadata, player):
"""Handle metadata changes"""
# Update tooltip with current track info
@@ -276,6 +388,9 @@ class MusicPlayer(Box):
self._current_track_info = "Music Player"
self.event_box.set_tooltip_text("Music Player")
# Update visibility when metadata changes (media content appears/disappears)
self.update_visibility()
def update_playback_state(self):
"""Update the visual state based on current playback"""
if not self.current_player:
@@ -296,7 +411,7 @@ class MusicPlayer(Box):
else:
# Create popup on-demand if it doesn't exist
if not self.popup:
self.popup = MusicPlayerPopup()
self.popup = MusicPlayerPopup(dock_component=self)
self.popup.show_popup()
def on_scroll(self, widget, event):
@@ -320,11 +435,47 @@ class MusicPlayer(Box):
return True
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
player = Playerctl.Player.new_from_name(player_name)
# Check if player has metadata indicating actual media content
if hasattr(player, 'props') and hasattr(player.props, 'metadata'):
metadata = player.props.metadata
if metadata and len(metadata.keys()) > 0:
# Check for essential metadata that indicates real media
keys = metadata.keys()
if ("xesam:title" in keys or
"xesam:artist" in keys or
"mpris:length" in keys):
print(f"[DEBUG] Found player with media content: {player.props.player_name}")
return True
# 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
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
def update_visibility(self):
"""Update component visibility based on available players"""
# Show component only if there are active players
has_players = self.current_player is not None
self.set_visible(has_players)
"""Update component visibility based on players with actual media content"""
# Show component only if there are players with actual media content
has_media_players = self.has_running_players()
self.set_visible(has_media_players)
# Hide in vertical mode if user preference is set
if data.VERTICAL and hasattr(data, "DOCK_COMPONENTS_VISIBILITY"):
+416
View File
@@ -0,0 +1,416 @@
from fabric.widgets.box import Box
from fabric.widgets.label import Label
from fabric.widgets.centerbox import CenterBox
from fabric.widgets.button import Button
from fabric.widgets.stack import Stack
from fabric.core.service import Signal
from services.mpris import PlayerManager, PlayerService
from utils.wiggle_bar import WigglyWidget
import utils.icons as icons
import config.data as data
from loguru import logger
class Player(Box):
def __init__(self, player, **kwargs):
super().__init__(style_classes="player", orientation="v", **kwargs)
# Check if we're in vertical mode based on dock position
vertical_mode = data.DOCK_POSITION in ["Left", "Right"] if hasattr(data, 'DOCK_POSITION') else False
if not vertical_mode:
# vertical class binding from unknown source
self.remove_style_class("vertical")
self._player = PlayerService(player=player)
self.duration = 0.0
self._player.connect("pause", self.on_pause)
self._player.connect("play", self.on_play)
self._player.connect("meta-change", self.on_metadata)
self._player.connect("shuffle-toggle", self.on_shuffle)
self._player.connect("track-position", self.on_update_track_position)
self.player_name = Label(
name=player.props.player_name,
style_classes="player-icon",
markup=getattr(icons, player.props.player_name, icons.disc),
)
# Use a fallback background image
self.set_style(
f"background-image:url('{data.HOME_DIR}/.current.wall')"
)
self.song = Label(
name="song",
label="song",
justification="left",
h_align="start",
max_chars_width=10,
)
self.artist = Label(
name="artist", label="artist", justification="left", h_align="start"
)
self.music = Box(
name="music",
orientation="v",
h_expand=True,
v_expand=True,
children=[self.song, self.artist],
)
self.play_pause_button = Button(
name="pause-button",
child=Label(name="pause-label", markup=icons.play),
style_classes="pause-track",
tooltip_text="Play/Pause",
on_clicked=lambda b, *_: self.handle_play_pause(player),
)
self.shuffle_button = Button(
name="shuffle-button",
child=Label(name="shuffle", markup=icons.shuffle),
on_clicked=lambda b, *_: self.handle_shuffle(b, player),
)
self.wiggly = WigglyWidget()
self.wiggly.connect("on-seek", self.on_seek)
self.gtk_wrapper = Box(
orientation="v",
h_expand=True,
v_expand=True,
h_align="fill",
v_align="fill",
children=self.wiggly,
)
self.children = [
Box(name="source", h_expand=True, v_expand=True, children=self.player_name),
CenterBox(
name="details",
start_children=self.music,
end_children=self.play_pause_button if not vertical_mode else [],
),
Box(
name="controls",
style_classes="horizontal" if not vertical_mode else "vertical",
spacing=5,
children=[
Button(
name="prev-button",
child=Label(name="play-previous", markup=icons.prev),
on_clicked=lambda b, *_: self.handle_prev(player),
),
CenterBox(
name="progress-container",
h_expand=True,
v_expand=True,
orientation="v",
center_children=[self.gtk_wrapper],
),
Button(
name="next-button",
child=Label(name="play-next", markup=icons.next),
on_clicked=lambda b, *_: self.handle_next(player),
),
self.shuffle_button,
]
if not vertical_mode
else [
CenterBox(
orientation="v",
h_expand=True,
start_children=[
CenterBox(
h_expand=True,
v_expand=True,
v_align="end",
start_children=[
Button(
name="prev-button",
child=Label(
name="play-previous", markup=icons.prev
),
on_clicked=lambda b, *_: self.handle_prev(
player
),
),
Button(
name="next-button",
child=Label(
name="play-next", markup=icons.next
),
on_clicked=lambda b, *_: self.handle_next(
player
),
),
self.shuffle_button,
],
end_children=self.play_pause_button,
)
],
end_children=CenterBox(
name="progress-container",
h_expand=True,
v_expand=True,
orientation="v",
center_children=[self.gtk_wrapper],
),
)
],
),
]
self.on_metadata(self._player, metadata=player.props.metadata, player=player)
def on_update_track_position(self, sender, pos, dur):
self.duration = dur
if dur > 0:
self.wiggly.update_value_from_signal(pos / dur)
def on_seek(self, sender, ratio):
pos = ratio * self.duration # duration in seconds
print(f"Seeking to {pos:.2f}s")
self._player.set_position(int(pos))
def on_metadata(self, sender, metadata, player):
keys = metadata.keys()
if "xesam:artist" in keys and "xesam:title" in keys:
# Check if we're in vertical mode
vertical_mode = data.DOCK_POSITION in ["Left", "Right"] if hasattr(data, 'DOCK_POSITION') else False
_max_chars = 43 if not vertical_mode else 30
song_title = metadata["xesam:title"]
if len(song_title) > _max_chars:
song_title = song_title[: _max_chars - 1] + "…"
self.song.set_label(song_title)
artist_list = metadata["xesam:artist"]
artist_name = artist_list[0] if artist_list else "Unknown Artist"
if len(artist_name) > _max_chars:
artist_name = artist_name[: _max_chars - 1] + "…"
self.artist.set_label(artist_name)
if "mpris:artUrl" in keys:
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:
video_id = match.group(1)
thumbnail_url = f"https://img.youtube.com/vi/{video_id}/hqdefault.jpg"
self.set_style(f"background-image:url('{thumbnail_url}')")
print(f"Using YouTube thumbnail: {thumbnail_url}")
else:
# Fallback to default wallpaper
self.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')")
else:
# Fallback to default wallpaper when no artwork is available
self.set_style(f"background-image:url('{data.HOME_DIR}/.current.wall')")
if (
player.props.playback_status.value_name
== "PLAYERCTL_PLAYBACK_STATUS_PLAYING"
):
self.on_play(self._player)
if player.props.shuffle == True:
self.shuffle_button.get_child().set_markup(icons.shuffle) # Use same icon for now
self.shuffle_button.get_child().set_name("disable-shuffle")
else:
self.shuffle_button.get_child().set_markup(icons.shuffle)
self.shuffle_button.get_child().set_name("shuffle")
def on_pause(self, sender):
self.play_pause_button.get_child().set_markup(icons.play)
self.play_pause_button.get_child().set_name("pause-label")
self.wiggly.dragging = True
self.wiggly.update_amplitude(True)
self.wiggly.pause = True
self.play_pause_button.add_style_class("pause-track")
def on_play(self, sender):
self.play_pause_button.get_child().set_markup(icons.pause)
self.play_pause_button.get_child().set_name("play-label")
self.wiggly.pause = False
self.wiggly.dragging = False
self.wiggly.update_amplitude(False)
self.play_pause_button.remove_style_class("pause-track")
def on_shuffle(self, sender, player, status):
print("callback status", status)
if status == False:
self.shuffle_button.get_child().set_markup(icons.shuffle)
self.shuffle_button.get_child().set_name("shuffle")
else:
self.shuffle_button.get_child().set_markup(icons.shuffle) # Use same icon for now
self.shuffle_button.get_child().set_name("disable-shuffle")
self.shuffle_button.get_child().set_style("color: white")
def handle_next(self, player):
player.next()
def handle_prev(self, player):
player.previous()
def handle_play_pause(self, player):
is_playing = self.play_pause_button.get_child().get_name() == "play-label"
def _set_play_ui():
self.play_pause_button.get_child().set_markup(icons.pause)
self.play_pause_button.remove_style_class("pause-track")
self.play_pause_button.get_child().set_name("pause-label")
def _set_pause_ui():
self.play_pause_button.get_child().set_markup(icons.play)
self.play_pause_button.add_style_class("pause-track")
self.play_pause_button.get_child().set_name("play-label")
if is_playing:
_set_pause_ui()
else:
_set_play_ui()
try:
player.play_pause()
except Exception as e:
# revert if signal failed
if is_playing:
_set_pause_ui()
else:
_set_play_ui()
logger.warning("Failed to toggle playback:", e)
def handle_shuffle(self, shuffle_button, player):
print("shuffle", player.props.shuffle)
if player.props.shuffle == False:
player.set_shuffle(True)
print("setting to true", player.props.player_name)
else:
player.set_shuffle(False)
shuffle_button.get_child().set_style("color: var(--outline)")
class PlayerContainer(Box):
@Signal
def active_player_changed(self, player) -> None: ...
def __init__(self, **kwargs):
super().__init__(name="player-container", orientation="v", **kwargs)
self.manager = PlayerManager()
self.manager.connect("new-player", self.new_player)
self.manager.connect("player-vanish", self.on_player_vanish)
self.stack = Stack(
name="player-container",
transition_type="crossfade",
transition_duration=100,
children=[],
)
self.player_stack = Stack(
name="player-stack",
transition_type="crossfade",
transition_duration=100,
children=[],
)
# Check if we're in vertical mode
vertical_mode = data.DOCK_POSITION in ["Left", "Right"] if hasattr(data, 'DOCK_POSITION') else False
self.player_switch_container = CenterBox(
name="player-switch-container",
orientation="h",
style_classes="horizontal-player"
if not vertical_mode
else "vertical-player",
center_children=[],
)
self.children = [self.stack, self.player_switch_container]
self.player = []
self.player_objects = {} # Map player names to player objects
self.active_player = None # Track the currently active player
self.manager.init_all_players()
def new_player(self, manager, player):
print(player.props.player_name, "new player")
print(player)
new_player = Player(player=player)
new_player.gtk_wrapper.queue_draw()
new_player.set_name(player.props.player_name)
self.player.append(new_player)
self.player_objects[player.props.player_name] = player # Store the player object
print("stacking player")
self.stack.add_named(new_player, player.props.player_name)
self.player_switch_container.add_center(
Button(
name=player.props.player_name,
style_classes="player-button",
on_clicked=lambda b: self.switch_player(player.props.player_name, b),
)
)
# If this is the first player, make it active
if not self.active_player:
self.active_player = player
self.active_player_changed(player)
self.update_player_list()
def switch_player(self, player_name, button):
self.stack.set_visible_child_name(player_name)
for btn in self.player_switch_container.center_children:
btn.remove_style_class("active")
button.add_style_class("active")
# Get the player object and emit signal
if player_name in self.player_objects:
self.active_player = self.player_objects[player_name]
self.active_player_changed(self.active_player)
def on_player_vanish(self, manager, player):
player_name = player.props.player_name
for player_instance in self.player:
if player_instance.get_name() == player_name:
self.stack.remove(player_instance)
self.player.remove(player_instance)
for btn in self.player_switch_container.center_children:
if btn.get_name() == player_name:
self.player_switch_container.remove_center(btn)
break
# Clean up player objects dictionary
if player_name in self.player_objects:
del self.player_objects[player_name]
# If the active player vanished, switch to another one
if self.active_player and self.active_player.props.player_name == player_name:
self.active_player = None
# Try to find another player to make active
if self.player_objects:
first_player_name = next(iter(self.player_objects))
self.active_player = self.player_objects[first_player_name]
self.active_player_changed(self.active_player)
self.update_player_list()
def update_player_list(self):
curr = self.stack.get_visible_child()
if curr:
for btn in self.player_switch_container.center_children:
print(btn.get_name())
if btn.get_name() == curr.get_name():
btn.add_style_class("active")
else:
btn.remove_style_class("active")