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