aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--deckit/modules/music.py49
-rw-r--r--tests/test_music.py30
2 files changed, 79 insertions, 0 deletions
diff --git a/deckit/modules/music.py b/deckit/modules/music.py
new file mode 100644
index 0000000..af13688
--- /dev/null
+++ b/deckit/modules/music.py
@@ -0,0 +1,49 @@
+"""Now playing and volume, via playerctl and wpctl."""
+
+import subprocess
+
+from ..module import Module
+
+SEP = "\x1f"
+
+BASE_ACTIONS = {
+ "voldown": ["wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-"],
+ "mute": ["wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle"],
+ "volup": ["wpctl", "set-volume", "-l", "1.0", "@DEFAULT_AUDIO_SINK@", "5%+"],
+ "prev": ["playerctl", "previous"],
+ "play": ["playerctl", "play-pause"],
+ "next": ["playerctl", "next"],
+}
+
+
+def _run(cmd, timeout=2):
+ try:
+ return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout).stdout.strip()
+ except (OSError, subprocess.TimeoutExpired):
+ return ""
+
+
+def _popen(cmd):
+ subprocess.Popen(cmd, start_new_session=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+
+
+def make(cfg: dict) -> Module:
+ run = cfg.get("run", _run)
+
+ def state():
+ fmt = SEP.join(["{{status}}", "{{title}}", "{{artist}}", "{{album}}", "{{mpris:length}}",
+ "{{position}}", "{{mpris:artUrl}}", "{{playerName}}"])
+ out = run(["playerctl", "metadata", "--format", fmt])
+ vol = run(["wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@"])
+ volume = {"level": round(float(vol.split()[1]) * 100) if len(vol.split()) > 1 else 0,
+ "muted": "MUTED" in vol}
+ if not out:
+ return {"status": "Stopped", "volume": volume}
+ status, title, artist, album, length, pos, art, name = (out.split(SEP) + [""] * 8)[:8]
+ art_src = art if art.startswith(("http://", "https://")) else None
+ return {"status": status, "title": title, "artist": artist, "album": album,
+ "length": int(length or 0) / 1e6, "position": int(pos or 0) / 1e6,
+ "art": art_src, "player": name.split(".")[0], "volume": volume}
+
+ actions = {name: (lambda cmd: (lambda body: _popen(cmd)))(cmd) for name, cmd in BASE_ACTIONS.items()}
+ return Module(name="music", title="Music", state=state, actions=actions)
diff --git a/tests/test_music.py b/tests/test_music.py
new file mode 100644
index 0000000..675e72b
--- /dev/null
+++ b/tests/test_music.py
@@ -0,0 +1,30 @@
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+import unittest
+
+import deckit.modules.music as music
+
+
+class Music(unittest.TestCase):
+ def test_parses_metadata(self):
+ sep = "\x1f"
+ fake = lambda cmd, timeout=2: {
+ "metadata": sep.join(["Playing", "Song", "Artist", "Album", "200000000",
+ "1000000", "", "spotify"]),
+ "get-volume": "Volume: 0.42",
+ }[cmd[1]]
+ m = music.make({"run": fake})
+ st = m.state()
+ self.assertEqual(st["title"], "Song")
+ self.assertEqual(st["status"], "Playing")
+ self.assertEqual(st["volume"]["level"], 42)
+
+ def test_action_commands(self):
+ self.assertEqual(music.BASE_ACTIONS["next"], ["playerctl", "next"])
+
+
+if __name__ == "__main__":
+ unittest.main()