From 65862ce3ae0485874569f9b1e9ce967c37492313 Mon Sep 17 00:00:00 2001 From: jim kolpack Date: Mon, 28 Sep 2026 22:53:18 -0400 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20SUB/WAVE=20Now?= =?UTF-8?q?=20Playing=20Tronbyt=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 ++ README.md | 47 +++++++++++++++++++ manifest.yaml | 8 ++++ subwave_nowplaying.star | 101 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 manifest.yaml create mode 100644 subwave_nowplaying.star diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d64e56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.starcache/ +*.gif +*.webp diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fdcf27 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# subwave-nowplaying + +Tronbyt/Tidbyt app showing the current track on a [SUB/WAVE](https://github.com/perminder-klair/subwave) +internet radio station: album art, track title, artist, and station name, +with title/artist scrolling if they don't fit. + +Not tied to any specific station — set your own station's base URL in the +app's config when adding it to a device. + +## Configuration + +- **Station URL** — the base URL of your SUB/WAVE instance, e.g. + `https://radio.example.com`. No trailing slash needed (stripped + automatically). No auth required — SUB/WAVE's now-playing and cover-art + endpoints are public. + +## How it works + +- `GET {station_url}/api/now-playing` — track title, artist, `subsonic_id`, + DJ/station name, and stream online status. Cached 20s. +- `GET {station_url}/api/cover/{subsonic_id}` — album art JPEG, proxied by + SUB/WAVE itself (no Navidrome credentials needed). Cached 300s. +- If the station reports `streamOnline: false`, shows " offline" + instead of stale track info. +- If a track has no `subsonic_id`, or the cover fetch fails, falls back to a + full-width layout with no art rather than a broken image. + +**Note on unreachable hosts:** if the station URL is wrong or the host is +completely unreachable (DNS failure, connection refused), `http.get` raises +a hard error and the render fails for that cycle — this is normal pixlet/ +Starlark behavior (no try/except in the language), not a bug. Only +HTTP-level errors (404, 500, etc.) are something the app can catch and +display a message for, which it does. + +## Dev/test + +``` +pixlet render subwave_nowplaying.star station_url=https://your-station.example.com +pixlet serve subwave_nowplaying.star +``` + +## Deploy + +Upload `subwave_nowplaying.star` directly through your Tronbyt server's +"Upload app" button on a device's Add App page (single file, no bundled +assets — no need to zip). Configure the Station URL field when adding it to +your device's schedule. diff --git a/manifest.yaml b/manifest.yaml new file mode 100644 index 0000000..5e3a407 --- /dev/null +++ b/manifest.yaml @@ -0,0 +1,8 @@ +--- +id: subwave-nowplaying +name: SUB/WAVE Now Playing +summary: Shows what's playing on a SUB/WAVE internet radio station +desc: Configure with your SUB/WAVE (github.com/perminder-klair/subwave) station's base URL to display the current track title, artist, station name, and album art. +author: poprhythm +fileName: subwave_nowplaying.star +packageName: subwave-nowplaying diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star new file mode 100644 index 0000000..5bd863f --- /dev/null +++ b/subwave_nowplaying.star @@ -0,0 +1,101 @@ +""" +Applet: SUB/WAVE Now Playing +Summary: Shows what's playing on a SUB/WAVE station +Description: Configure with your SUB/WAVE (github.com/perminder-klair/subwave) station's base URL to display the current track, artist, and album art. +Author: poprhythm +""" + +load("render.star", "render") +load("schema.star", "schema") +load("http.star", "http") + +DEFAULT_STATION_URL = "" +NOWPLAYING_TTL = 20 +COVER_TTL = 300 + +def main(config): + station_url = config.get("station_url", DEFAULT_STATION_URL) + if station_url: + station_url = station_url.rstrip("/") + + if not station_url: + return render.Root(child = _message_screen(["Set station URL", "in app settings"])) + + now = _fetch_now_playing(station_url) + if now == None: + return render.Root(child = _message_screen(["Can't reach", "station"])) + + if not now["online"]: + return render.Root(child = _message_screen([now["station"], "offline"])) + + art = _fetch_cover(station_url, now["subsonic_id"]) + + text_column = render.Column( + children = [ + render.Marquee(width = 32, child = render.Text(now["title"])), + render.Marquee(width = 32, child = render.Text(now["artist"], color = "#aaaaaa")), + render.Marquee(width = 32, child = render.Text(now["station"], font = "tom-thumb", color = "#666666")), + ], + ) + + if art: + content = render.Row(children = [render.Image(src = art, width = 32, height = 32), text_column]) + else: + content = render.Column( + main_align = "center", + cross_align = "center", + expanded = True, + children = [ + render.Marquee(width = 64, child = render.Text(now["title"])), + render.Marquee(width = 64, child = render.Text(now["artist"], color = "#aaaaaa")), + ], + ) + + return render.Root(child = content) + +def _message_screen(lines): + return render.Column( + main_align = "center", + cross_align = "center", + expanded = True, + children = [render.Text(line, font = "tom-thumb") for line in lines], + ) + +def _fetch_now_playing(station_url): + rep = http.get(station_url + "/api/now-playing", ttl_seconds = NOWPLAYING_TTL) + if rep.status_code != 200: + return None + + data = rep.json() + np = data.get("nowPlaying", {}) + dj = data.get("dj", {}) + + return { + "title": np.get("title", "Unknown title"), + "artist": np.get("artist", "Unknown artist"), + "subsonic_id": np.get("subsonic_id", ""), + "station": dj.get("station", "SUB/WAVE"), + "online": bool(data.get("streamOnline", False)), + } + +def _fetch_cover(station_url, subsonic_id): + if not subsonic_id: + return None + rep = http.get(station_url + "/api/cover/" + subsonic_id, ttl_seconds = COVER_TTL) + if rep.status_code != 200: + return None + return rep.body() + +def get_schema(): + return schema.Schema( + version = "1", + fields = [ + schema.Text( + id = "station_url", + name = "Station URL", + desc = "Base URL of your SUB/WAVE station, e.g. https://radio.example.com", + icon = "server", + default = DEFAULT_STATION_URL, + ), + ], + )