From 15e384bc1d97c84df0545555bf4c298702c9eacb Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 19:08:51 +0100 Subject: feat: card headers, music art/seek, clearer build card --- deckit/modules/music.py | 19 ++++++++++++++++--- deckit/modules/sbbuilder.py | 2 +- deckit/server.py | 25 ++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) (limited to 'deckit') diff --git a/deckit/modules/music.py b/deckit/modules/music.py index af13688..f0baacb 100644 --- a/deckit/modules/music.py +++ b/deckit/modules/music.py @@ -2,7 +2,7 @@ import subprocess -from ..module import Module +from ..module import Module, ModuleError SEP = "\x1f" @@ -29,6 +29,7 @@ def _popen(cmd): 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}}", @@ -40,10 +41,22 @@ def make(cfg: dict) -> Module: 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 + 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} - actions = {name: (lambda cmd: (lambda body: _popen(cmd)))(cmd) for name, cmd in BASE_ACTIONS.items()} + 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) diff --git a/deckit/modules/sbbuilder.py b/deckit/modules/sbbuilder.py index 3a48a1d..41f3a14 100644 --- a/deckit/modules/sbbuilder.py +++ b/deckit/modules/sbbuilder.py @@ -33,4 +33,4 @@ def make(cfg: dict) -> Module: return {"offline": True} return {"offline": False, "jobs": data.get("jobs", [])} - return Module(name="sbbuilder", title="Builds", state=state) + return Module(name="sbbuilder", title="Build VM", state=state) diff --git a/deckit/server.py b/deckit/server.py index bddbb00..638322f 100644 --- a/deckit/server.py +++ b/deckit/server.py @@ -11,11 +11,12 @@ import mimetypes import os import secrets import socketserver +import subprocess import threading from http.cookies import CookieError, SimpleCookie from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path -from urllib.parse import parse_qs, urlsplit +from urllib.parse import parse_qs, unquote, urlparse, urlsplit from . import config, module, theme @@ -39,6 +40,25 @@ def token_path() -> Path: return config.user_config_path().parent / "token" +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 art(): + """The current track's cover art, if the player exposes a local file.""" + url = _run(["playerctl", "metadata", "mpris:artUrl"]) + if not url.startswith("file://"): + return None + path = Path(unquote(urlparse(url).path)) + try: + return path.read_bytes(), mimetypes.guess_type(path)[0] or "image/png" + except OSError: + return None + + def load_token() -> str: """A per-machine secret the tablet's browser holds as a cookie.""" path = token_path() @@ -98,6 +118,9 @@ class Handler(BaseHTTPRequestHandler): if not self._authed(): return self._send(401) parts = path.strip("/").split("/") + if len(parts) == 2 and parts[1] == "art": + image = art() + return self._send(200, image[0], image[1], cache=True) if image else self._send(404) if len(parts) == 2 and parts[1] == "modules": return self._json(REGISTRY.names()) if len(parts) == 2: -- cgit v1.2.3