127 lines
4 KiB
Python
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()
|