Add show name and track progress bar
This commit is contained in:
1 parent
65862ce3ae
commit
1d2021e6f2
2 files changed
+70
-10
No files matched your search
@@ -1,8 +1,9 @@
|
|||||||
# subwave-nowplaying
|
# subwave-nowplaying
|
||||||
|
|
||||||
Tronbyt/Tidbyt app showing the current track on a [SUB/WAVE](https://github.com/perminder-klair/subwave)
|
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,
|
internet radio station: album art, track title, artist, the current show
|
||||||
with title/artist scrolling if they don't fit.
|
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
|
Not tied to any specific station — set your own station's base URL in the
|
||||||
app's config when adding it to a device.
|
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.
|
instead of stale track info.
|
||||||
- If a track has no `subsonic_id`, or the cover fetch fails, falls back to a
|
- 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.
|
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
|
**Note on unreachable hosts:** if the station URL is wrong or the host is
|
||||||
completely unreachable (DNS failure, connection refused), `http.get` raises
|
completely unreachable (DNS failure, connection refused), `http.get` raises
|
||||||
|
|||||||
+55
-8
@@ -1,13 +1,15 @@
|
|||||||
"""
|
"""
|
||||||
Applet: SUB/WAVE Now Playing
|
Applet: SUB/WAVE Now Playing
|
||||||
Summary: Shows what's playing on a SUB/WAVE station
|
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
|
Author: poprhythm
|
||||||
"""
|
"""
|
||||||
|
|
||||||
load("render.star", "render")
|
load("render.star", "render")
|
||||||
load("schema.star", "schema")
|
load("schema.star", "schema")
|
||||||
load("http.star", "http")
|
load("http.star", "http")
|
||||||
|
load("cache.star", "cache")
|
||||||
|
load("time.star", "time")
|
||||||
|
|
||||||
DEFAULT_STATION_URL = ""
|
DEFAULT_STATION_URL = ""
|
||||||
NOWPLAYING_TTL = 20
|
NOWPLAYING_TTL = 20
|
||||||
@@ -29,14 +31,18 @@ def main(config):
|
|||||||
return render.Root(child = _message_screen([now["station"], "offline"]))
|
return render.Root(child = _message_screen([now["station"], "offline"]))
|
||||||
|
|
||||||
art = _fetch_cover(station_url, now["subsonic_id"])
|
art = _fetch_cover(station_url, now["subsonic_id"])
|
||||||
|
progress = _track_progress(now["subsonic_id"], now["duration"])
|
||||||
|
|
||||||
text_column = render.Column(
|
lines = [
|
||||||
children = [
|
render.Marquee(width = 32, child = render.Text(now["title"])),
|
||||||
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["artist"], color = "#aaaaaa")),
|
]
|
||||||
render.Marquee(width = 32, child = render.Text(now["station"], font = "tom-thumb", color = "#666666")),
|
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:
|
if art:
|
||||||
content = render.Row(children = [render.Image(src = art, width = 32, height = 32), text_column])
|
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],
|
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):
|
def _fetch_now_playing(station_url):
|
||||||
rep = http.get(station_url + "/api/now-playing", ttl_seconds = NOWPLAYING_TTL)
|
rep = http.get(station_url + "/api/now-playing", ttl_seconds = NOWPLAYING_TTL)
|
||||||
if rep.status_code != 200:
|
if rep.status_code != 200:
|
||||||
@@ -69,12 +85,15 @@ def _fetch_now_playing(station_url):
|
|||||||
data = rep.json()
|
data = rep.json()
|
||||||
np = data.get("nowPlaying", {})
|
np = data.get("nowPlaying", {})
|
||||||
dj = data.get("dj", {})
|
dj = data.get("dj", {})
|
||||||
|
active_show = data.get("context", {}).get("activeShow", {})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"title": np.get("title", "Unknown title"),
|
"title": np.get("title", "Unknown title"),
|
||||||
"artist": np.get("artist", "Unknown artist"),
|
"artist": np.get("artist", "Unknown artist"),
|
||||||
"subsonic_id": np.get("subsonic_id", ""),
|
"subsonic_id": np.get("subsonic_id", ""),
|
||||||
|
"duration": np.get("duration", 0),
|
||||||
"station": dj.get("station", "SUB/WAVE"),
|
"station": dj.get("station", "SUB/WAVE"),
|
||||||
|
"show": active_show.get("name", ""),
|
||||||
"online": bool(data.get("streamOnline", False)),
|
"online": bool(data.get("streamOnline", False)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +105,34 @@ def _fetch_cover(station_url, subsonic_id):
|
|||||||
return None
|
return None
|
||||||
return rep.body()
|
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():
|
def get_schema():
|
||||||
return schema.Schema(
|
return schema.Schema(
|
||||||
version = "1",
|
version = "1",
|
||||||
|
|||||||