playerctl-gtk/playerctl_gui.py
fwastring 3b2edf9af2 fix
2025-10-12 14:37:23 +02:00

127 lines
4 KiB
Python

#!/usr/bin/env python3
import subprocess
from typing import Optional
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import GLib, Gtk
METADATA_FORMAT = "{{{{status}}}} — {{{{artist}}}} — {{{{title}}}}"
class PlayerctlApp(Gtk.Application):
def __init__(self) -> None:
super().__init__(application_id="dev.codex.PlayerctlGUI")
self._label: Optional[Gtk.Label] = None
def do_activate(self, *args) -> None: # type: ignore[override]
if self.props.active_window:
self.props.active_window.present()
return
window = Gtk.ApplicationWindow(application=self)
window.set_title("Playerctl Controls")
window.set_border_width(12)
window.set_default_size(320, 120)
controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
controls.set_homogeneous(True)
controls.set_hexpand(True)
controls.set_vexpand(True)
controls.set_valign(Gtk.Align.FILL)
prev_button = Gtk.Button.new_from_icon_name(
"media-skip-backward",
Gtk.IconSize.LARGE_TOOLBAR,
)
prev_button.connect("clicked", self._on_prev_clicked)
prev_button.set_hexpand(True)
prev_button.set_vexpand(True)
prev_button.set_valign(Gtk.Align.FILL)
controls.pack_start(prev_button, True, True, 0)
toggle_button = Gtk.Button.new_from_icon_name(
"media-playback-start",
Gtk.IconSize.LARGE_TOOLBAR,
)
toggle_button.connect("clicked", self._on_toggle_clicked)
toggle_button.set_hexpand(True)
toggle_button.set_vexpand(True)
toggle_button.set_valign(Gtk.Align.FILL)
controls.pack_start(toggle_button, True, True, 0)
next_button = Gtk.Button.new_from_icon_name(
"media-skip-forward",
Gtk.IconSize.LARGE_TOOLBAR,
)
next_button.connect("clicked", self._on_next_clicked)
next_button.set_hexpand(True)
next_button.set_vexpand(True)
next_button.set_valign(Gtk.Align.FILL)
controls.pack_start(next_button, True, True, 0)
layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
layout.pack_start(controls, True, True, 0)
window.add(layout)
window.show_all()
GLib.timeout_add_seconds(2, self._refresh_metadata)
self._refresh_metadata()
def _on_toggle_clicked(self, _button: Gtk.Button) -> None:
self._run_playerctl(["play-pause"])
GLib.idle_add(self._refresh_metadata)
def _on_next_clicked(self, _button: Gtk.Button) -> None:
self._run_playerctl(["next"])
GLib.idle_add(self._refresh_metadata)
def _on_prev_clicked(self, _button: Gtk.Button) -> None:
self._run_playerctl(["previous"])
GLib.idle_add(self._refresh_metadata)
def _refresh_metadata(self) -> bool:
if not self._label:
return False
metadata = self._run_playerctl(["metadata", "--format", METADATA_FORMAT])
if metadata is None or not metadata.strip():
status = self._run_playerctl(["status"])
if status is None:
self._label.set_text("No active media player detected")
else:
self._label.set_text(status.strip())
else:
self._label.set_text(metadata.strip())
return True
def _run_playerctl(self, args: list[str]) -> Optional[str]:
cmd = ["playerctl", *args]
try:
completed = subprocess.run(
cmd,
check=False,
capture_output=True,
text=True,
timeout=5,
)
except (FileNotFoundError, subprocess.SubprocessError):
if self._label:
self._label.set_text("playerctl command not available")
return None
if completed.returncode != 0:
return None
return completed.stdout
def main() -> None:
app = PlayerctlApp()
app.run()
if __name__ == "__main__":
main()