# deck-it - music module # Copyright (C) 2026 Danilo M. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """Now playing and volume, via playerctl and wpctl.""" import subprocess from ..module import Module, ModuleError 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) popen = cfg.get("popen", _popen) 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] if art.startswith(("http://", "https://")): art_src = art elif art.startswith("file://"): art_src = "/api/art?v=" + str(abs(hash(art))) else: art_src = 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} def seek(body): pos = body.get("position") if not isinstance(pos, (int, float)) or pos < 0: raise ModuleError("position must be a number") popen(["playerctl", "position", str(round(float(pos), 1))]) actions = {name: (lambda cmd: (lambda body: popen(cmd)))(cmd) for name, cmd in BASE_ACTIONS.items()} actions["seek"] = seek return Module(name="music", title="Music", state=state, actions=actions)