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 signal
import socket
import time
import typing
import gi
import i3ipc
import i3ipc.aio
import psutil
@ -20,6 +22,11 @@ import rich.color
import rich.logging
import rich.terminal_theme
gi.require_version("Playerctl", "2.0")
import gi.repository.GLib
import gi.repository.Playerctl
logging.basicConfig(
level="DEBUG",
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))]
def clip(text: str, length: int = 30) -> str:
if len(text) > length:
text = text[: length - 1] + ""
return text
class ComposableText(typing.Generic[P, C]):
def __init__(
@ -140,7 +153,7 @@ class Section(ComposableText):
# Geometric series, with a cap
ANIM_A = 0.025
ANIM_R = 0.9
ANIM_MIN = 0.005
ANIM_MIN = 0.001
async def animate(self) -> None:
increment = 1 if self.size < self.targetSize else -1
@ -636,7 +649,7 @@ class I3WorkspacesProvider(MultiSectionsProvider):
else:
section.color = self.COLOR_DEFAULT
if workspace.focused or workspace.visible:
name = f"{name} X" # TODO Custom names
name = f"{name} X" # FIXME Custom names
section.setText(name)
return update
@ -680,6 +693,166 @@ class I3WorkspacesProvider(MultiSectionsProvider):
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):
COLOR_NORMAL = rich.color.Color.parse("green")
COLOR_WARNING = rich.color.Color.parse("yellow")
@ -1089,7 +1262,7 @@ async def main() -> None:
alignment=Alignment.CENTER,
)
bar.addProvider(
StaticProvider(text="mpris", color=color("bright_white")),
MprisProvider(color=color("bright_white")),
screenNum=rightPreferred,
alignment=Alignment.CENTER,
)

View file

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