frobar-ng: Broken mpris!
This commit is contained in:
parent
ffd402f57c
commit
f22b42a711
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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 = ./.;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue