diff --git a/README.md b/README.md index 7fdcf27..5894ca6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # 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. +internet radio station: album art, track title, artist, the current show +name, and a progress bar for the track, 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. @@ -24,6 +25,18 @@ app's config when adding it to a device. 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. +- Shows the current show name (`context.activeShow.name`) below the artist, + and a track progress bar at the bottom. + +**Progress bar caveat:** the now-playing API reports track `duration` but +not elapsed/remaining time, so the app tracks playback position itself — +the first time it sees a given `subsonic_id`, it stamps the current +wall-clock time in pixlet's cache, then diffs against that stamp on later +renders to compute elapsed seconds. This only works correctly on a +long-lived server process with a persistent cache (true for +`tronbyt/server`'s in-memory cache) — a one-shot `pixlet render` CLI +invocation always starts a fresh process with an empty cache, so the bar +will always show 0% when testing locally. This is expected, not a bug. **Note on unreachable hosts:** if the station URL is wrong or the host is completely unreachable (DNS failure, connection refused), `http.get` raises diff --git a/subwave_nowplaying.star b/subwave_nowplaying.star index 5bd863f..0d4497d 100644 --- a/subwave_nowplaying.star +++ b/subwave_nowplaying.star @@ -1,13 +1,15 @@ """ 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. +Description: Configure with your SUB/WAVE (github.com/perminder-klair/subwave) station's base URL to display the current track, artist, show, album art, and a progress bar. Author: poprhythm """ load("render.star", "render") load("schema.star", "schema") load("http.star", "http") +load("cache.star", "cache") +load("time.star", "time") DEFAULT_STATION_URL = "" NOWPLAYING_TTL = 20 @@ -29,14 +31,18 @@ def main(config): return render.Root(child = _message_screen([now["station"], "offline"])) art = _fetch_cover(station_url, now["subsonic_id"]) + progress = _track_progress(now["subsonic_id"], now["duration"]) - 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")), - ], - ) + lines = [ + render.Marquee(width = 32, child = render.Text(now["title"])), + render.Marquee(width = 32, child = render.Text(now["artist"], color = "#aaaaaa")), + ] + if now["show"]: + lines.append(render.Marquee(width = 32, child = render.Text(now["show"], font = "tom-thumb", color = "#666666"))) + if progress != None: + lines.append(_progress_bar(progress["fraction"], width = 32)) + + text_column = render.Column(children = lines) if art: content = render.Row(children = [render.Image(src = art, width = 32, height = 32), text_column]) @@ -61,6 +67,16 @@ def _message_screen(lines): children = [render.Text(line, font = "tom-thumb") for line in lines], ) +def _progress_bar(fraction, width = 32, height = 2): + filled = int(width * fraction + 0.5) + filled = max(0, min(filled, width)) + children = [] + if filled > 0: + children.append(render.Box(width = filled, height = height, color = "#66bb6a")) + if filled < width: + children.append(render.Box(width = width - filled, height = height, color = "#333333")) + return render.Row(children = children) + def _fetch_now_playing(station_url): rep = http.get(station_url + "/api/now-playing", ttl_seconds = NOWPLAYING_TTL) if rep.status_code != 200: @@ -69,12 +85,15 @@ def _fetch_now_playing(station_url): data = rep.json() np = data.get("nowPlaying", {}) dj = data.get("dj", {}) + active_show = data.get("context", {}).get("activeShow", {}) return { "title": np.get("title", "Unknown title"), "artist": np.get("artist", "Unknown artist"), "subsonic_id": np.get("subsonic_id", ""), + "duration": np.get("duration", 0), "station": dj.get("station", "SUB/WAVE"), + "show": active_show.get("name", ""), "online": bool(data.get("streamOnline", False)), } @@ -86,6 +105,34 @@ def _fetch_cover(station_url, subsonic_id): return None return rep.body() +# The now-playing API reports track duration but not elapsed/remaining time, +# so we track playback position ourselves: the first time we see a given +# subsonic_id, we stamp the current wall-clock time in the cache; on later +# renders (while the cache entry is still alive) we diff against that stamp +# to get elapsed seconds. This only works on a long-lived server process +# with a persistent cache (confirmed true for tronbyt/server's in-memory +# cache) -- a one-shot `pixlet render` CLI call always starts a fresh +# process with an empty cache, so progress will always show 0% locally. +def _track_progress(subsonic_id, duration): + if not subsonic_id or not duration: + return None + + key = "subwave_track_start_" + subsonic_id + now_unix = time.now().unix + cached = cache.get(key) + + if cached == None: + cache.set(key, str(now_unix), ttl_seconds = int(duration) + 30) + start_unix = now_unix + else: + start_unix = int(cached) + + elapsed = max(0, now_unix - start_unix) + remaining = max(0, duration - elapsed) + fraction = min(1.0, float(elapsed) / float(duration)) + + return {"elapsed": elapsed, "remaining": remaining, "fraction": fraction} + def get_schema(): return schema.Schema( version = "1",