Initial commit — SUB/WAVE Now Playing Tronbyt app
This commit is contained in:
commit
65862ce3ae
4 files changed
+159
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
.starcache/
|
||||||
|
*.gif
|
||||||
|
*.webp
|
||||||
@@ -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 "<station> 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.
|
||||||
@@ -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
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||