frobar-ng: Broken mpris!

This commit is contained in:
Geoffrey Frogeye 2025-01-10 00:16:48 +01:00
parent ffd402f57c
commit f22b42a711
Signed by: geoffrey
GPG key ID: C72403E7F82E6AD8
2 changed files with 180 additions and 4 deletions

View file

@ -9,8 +9,10 @@ import logging
import os import os
import signal import signal
import socket import socket
import time
import typing import typing
import gi
import i3ipc import i3ipc
import i3ipc.aio import i3ipc.aio
import psutil import psutil
@ -20,6 +22,11 @@ import rich.color
import rich.logging import rich.logging
import rich.terminal_theme import rich.terminal_theme
gi.require_version("Playerctl", "2.0")
import gi.repository.GLib
import gi.repository.Playerctl
logging.basicConfig( logging.basicConfig(
level="DEBUG", level="DEBUG",
format="%(message)s", format="%(message)s",
@ -58,6 +65,12 @@ def ramp(p: float, states: str = " ▁▂▃▄▅▆▇█") -> str:
return states[-1] * int(d) + states[round(m * (len(states) - 1))] return states[-1] * int(d) + states[round(m * (len(states) - 1))]
def clip(text: str, length: int = 30) -> str:
if len(text) > length:
text = text[: length - 1] + ""
return text
class ComposableText(typing.Generic[P, C]): class ComposableText(typing.Generic[P, C]):
def __init__( def __init__(
@ -140,7 +153,7 @@ class Section(ComposableText):
# Geometric series, with a cap # Geometric series, with a cap
ANIM_A = 0.025 ANIM_A = 0.025
ANIM_R = 0.9 ANIM_R = 0.9
ANIM_MIN = 0.005 ANIM_MIN = 0.001
async def animate(self) -> None: async def animate(self) -> None:
increment = 1 if self.size < self.targetSize else -1 increment = 1 if self.size < self.targetSize else -1
@ -636,7 +649,7 @@ class I3WorkspacesProvider(MultiSectionsProvider):
else: else:
section.color = self.COLOR_DEFAULT section.color = self.COLOR_DEFAULT
if workspace.focused or workspace.visible: if workspace.focused or workspace.visible:
name = f"{name} X" # TODO Custom names name = f"{name} X" # FIXME Custom names
section.setText(name) section.setText(name)
return update return update
@ -680,6 +693,166 @@ class I3WorkspacesProvider(MultiSectionsProvider):
await self.i3.main() await self.i3.main()
class MprisProvider(MirrorProvider):
STATUSES = {
gi.repository.Playerctl.PlaybackStatus.PLAYING: "",
gi.repository.Playerctl.PlaybackStatus.PAUSED: "",
gi.repository.Playerctl.PlaybackStatus.STOPPED: "",
}
PROVIDERS = {
"mpd": "",
"firefox": "",
"chromium": "",
"mpv": "",
}
@staticmethod
def get(
something: gi.overrides.GLib.Variant, key: str, default: typing.Any = None
) -> typing.Any:
if key in something.keys():
return something[key]
else:
return default
@staticmethod
def format_us(ms: int) -> str:
if ms < 60 * 60 * 1000000:
return time.strftime("%M:%S", time.gmtime(ms // 1000000))
else:
return str(datetime.timedelta(microseconds=ms))
def show_player(self, player_: gi.repository.Playerctl.Player) -> None:
player = player_.props.player_name
player = self.PROVIDERS.get(player, player)
status = self.STATUSES.get(player_.props.playback_status, "?")
self.status.setText(f"{player} {status}")
metadata = player_.props.metadata
album = self.get(metadata, "xesam:album")
if album:
self.album.setText(f"{clip(album)}")
else:
self.album.setText(None)
artists = self.get(metadata, "xesam:artist")
if artists:
artist = ", ".join(artists)
self.artist.setText(f"{clip(artist)}")
else:
self.artist.setText(None)
pos = player_.props.position # In µs
# FIXME Doesn't increment during play
text = f"{self.format_us(pos)}"
dur = self.get(metadata, "mpris:length")
if dur:
text += f"/{self.format_us(dur)}"
title = self.get(metadata, "xesam:title")
if title:
text += f" {clip(title)}"
self.title.setText(text)
def show_players(self, manager: gi.repository.Playerctl.PlayerManager, exclude: str | None = None) -> None:
# FIXME Opening and closing a chromium player, it will stay there (as default),
# which is not the behaviour of playerctl somehow
for name in manager.props.player_names:
if name == exclude: # DEBUG
continue
player = gi.repository.Playerctl.Player.new_from_name(name)
self.show_player(player)
break
else:
self.status.setText(None)
self.album.setText(None)
self.artist.setText(None)
self.title.setText(None)
def on_player_appear(
self,
manager: gi.repository.Playerctl.PlayerManager,
player: gi.repository.Playerctl.Player,
) -> None:
log.debug(f"Player appeared: {player.props.player_name}")
self.show_player(player)
def on_player_vanished(
self,
manager: gi.repository.Playerctl.PlayerManager,
player: gi.repository.Playerctl.Player,
) -> None:
log.debug(f"Player vanished: {player.props.player_name}")
self.show_player(player)
def on_event(
self,
player: gi.repository.Playerctl.Player,
_: typing.Any,
manager: gi.repository.Playerctl.PlayerManager,
) -> None:
log.debug(f"Player evented: {player.props.player_name}")
self.show_player(player)
def init_player(
self, manager: gi.repository.Playerctl.PlayerManager, name: str
) -> None:
player = gi.repository.Playerctl.Player.new_from_name(name)
# All events will cause the active player to change,
# so we listen on all events, even if the display won't change
player.connect("playback-status", self.on_event, manager)
player.connect("loop-status", self.on_event, manager)
player.connect("shuffle", self.on_event, manager)
player.connect("metadata", self.on_event, manager)
player.connect("volume", self.on_event, manager)
player.connect("seeked", self.on_event, manager)
manager.manage_player(player)
def on_name_appeared(
self, manager: gi.repository.Playerctl.PlayerManager, name: str
) -> None:
log.debug(f"Player name appeared: {name}")
self.init_player(manager, name)
def on_name_vanished(
self,
manager: gi.repository.Playerctl.PlayerManager,
name: str,
) -> None:
log.debug(f"Player name vanished: {name}")
self.show_players(manager, exclude=name)
def blocking(self) -> None:
manager = gi.repository.Playerctl.PlayerManager()
manager.connect("name-appeared", self.on_name_appeared)
manager.connect("player-appeared", self.on_player_appear)
manager.connect("name-vanished", self.on_name_vanished)
manager.connect("player-vanished", self.on_player_vanished)
for name in manager.props.player_names:
self.init_player(manager, name)
self.show_players(manager)
# FIXME Prevents graceful shutdown
main = gi.repository.GLib.MainLoop()
main.run()
async def run(self) -> None:
await super().run()
self.status = self.sectionType(parent=self.module, color=self.color)
self.album = self.sectionType(parent=self.module, color=self.color)
self.artist = self.sectionType(parent=self.module, color=self.color)
self.title = self.sectionType(parent=self.module, color=self.color)
# PyGObject can work with async code, but is experimental,
# also it running in its own event loop makes things tricky
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, self.blocking)
class AlertingProvider(Provider): class AlertingProvider(Provider):
COLOR_NORMAL = rich.color.Color.parse("green") COLOR_NORMAL = rich.color.Color.parse("green")
COLOR_WARNING = rich.color.Color.parse("yellow") COLOR_WARNING = rich.color.Color.parse("yellow")
@ -1089,7 +1262,7 @@ async def main() -> None:
alignment=Alignment.CENTER, alignment=Alignment.CENTER,
) )
bar.addProvider( bar.addProvider(
StaticProvider(text="mpris", color=color("bright_white")), MprisProvider(color=color("bright_white")),
screenNum=rightPreferred, screenNum=rightPreferred,
alignment=Alignment.CENTER, alignment=Alignment.CENTER,
) )

View file

@ -30,8 +30,9 @@ pkgs.python3Packages.buildPythonApplication rec {
mpd2 mpd2
notmuch notmuch
psutil psutil
pulsectl # old only
pulsectl-asyncio pulsectl-asyncio
pulsectl # old only
pygobject3
pyinotify pyinotify
rich rich
]; ];
@ -43,5 +44,7 @@ pkgs.python3Packages.buildPythonApplication rec {
]); ]);
makeWrapperArgs = [ "--prefix PATH : ${pkgs.lib.makeBinPath nativeBuildInputs}" ]; makeWrapperArgs = [ "--prefix PATH : ${pkgs.lib.makeBinPath nativeBuildInputs}" ];
GI_TYPELIB_PATH = pkgs.lib.makeSearchPath "lib/girepository-1.0" [ pkgs.glib.out pkgs.playerctl ];
src = ./.; src = ./.;
} }