Self-serving app: Radio Wien (Radio Vienna)

Hi,

as mentioned in this post (URL), I have set up my Tidbyt to serve some apps locally in my network with the help of my 24/7 server.

There are 2 more apps which can be found here (URL) and here (URL) for reference.

Let’s start with the main question: how can you self-serve apps to your Tidbyt.

First off, you have to install all the necessary tools that you need to develop apps - this can be found here: Tidbyt | Dev

Then you need a server that can run LaunchAgents via a crontab or similar (I’m using a Mac mini as the server and LaunchControl to write and run LaunchAgents on this server). I took some pointers from here (Pixlet install on linux/RaspberryPi). There are a few threads on the community discussion board that talk about setting up bash scripts and crontabs which I read and then came up with the following solution.

You need to write a bash script which serves the app on a regular basis:

#!/bin/bash
while true; do
	pixlet render /path/to/folder/radio_vienna_live.star
	pixlet push --api-token=“[YOUR API TOKEN HERE]” /path/to/folder/radio_vienna_live.webp
		wait_period=$(($wait_period+30))
		if [ $wait_period -ge 50400 ];then
			break
		else
			sleep 30
		fi
done

This script code renders the .star file called “VIE_Monitor” every 30 seconds for 14 hours (10800 / 30+30 / 60 = 14). I named the script “radiowien_render.sh” and saved it in a specific folder (same as the .star and .webp files).

Now this script needs to be started at a certain time automatically by the server in order to show up on your Tidbyt. This can be done with a crontab or a LaunchAgent.

So I set up a LaunchAgent which looked like this: (Never mind the red label on the top)

![image|690x479](upload://ufD2Rq2hWr3pCeCkpw0q0erhMdR.jpeg)

If you prefer the XML code:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Disabled</key>
	<false/>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin</string>
	</dict>
	<key>KeepAlive</key>
	<false/>
	<key>Label</key>
	<string>com.localhost.radiowien.push</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/sh</string>
		<string>/path/to/folder/radiowien_render.sh</string>
	</array>
	<key>StandardErrorPath</key>
	<string>/tmp/local.job.err</string>
	<key>StandardOutPath</key>
	<string>/tmp/local.job.out</string>
	<key>StartCalendarInterval</key>
	<array>
		<dict>
			<key>Hour</key>
			<integer>6</integer>
			<key>Minute</key>
			<integer>30</integer>
			<key>Weekday</key>
			<integer>1</integer>
		</dict>
		<dict>
			<key>Hour</key>
			<integer>6</integer>
			<key>Minute</key>
			<integer>30</integer>
			<key>Weekday</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>Hour</key>
			<integer>6</integer>
			<key>Minute</key>
			<integer>30</integer>
			<key>Weekday</key>
			<integer>3</integer>
		</dict>
		<dict>
			<key>Hour</key>
			<integer>6</integer>
			<key>Minute</key>
			<integer>30</integer>
			<key>Weekday</key>
			<integer>4</integer>
		</dict>
		<dict>
			<key>Hour</key>
			<integer>6</integer>
			<key>Minute</key>
			<integer>30</integer>
			<key>Weekday</key>
			<integer>5</integer>
		</dict>
		<dict>
			<key>Hour</key>
			<integer>9</integer>
			<key>Minute</key>
			<integer>0</integer>
			<key>Weekday</key>
			<integer>6</integer>
		</dict>
	</array>
</dict>
</plist>

This LaunchAgent starts every weekday at 6:30am and starts the radiowien_render.sh script which then runs for 14 hours and stops automatically.

Of course, the PATH key might be different for you depending on your setup (my installations are all standard, so if you haven’t tinkered with the setup it should work).

There is one small quirk: as I’m rendering several apps they sometimes “conflict”, meaning that they override each other leading to only short times on the screen of the Tidbyt. I haven’t figured out how to solve that but it’s basically only a nuisance not a showstopper.

And now for the pixlet code. (Note: I have used some code tibits from other apps - apologies for not being able to give credit where credit is due - I forgot where I copied it from…)

I have added some comments in the code - not sure if at all helpful for anyone. Screenshot at the end of the post.

"""
Applet: ORF Radio Wien Live
Summary: Show What's Playing and Album art
Description: Show title, artist and album art from Austria's Radio Wien/Vienna.
Author: Joerg Windbichler
"""

load("encoding/base64.star", "base64")
load("http.star", "http")
load("render.star", "render")
load("schema.star", "schema")
load("time.star", "time")

def main(config):
    timezone = config.get("timezone") or "Europe/Vienna"
    jetzt = time.now().in_location(timezone)
    now = time.now()
    metric = time.now().unix * 1000
  
    # Radio Wien live json - found via live player and web inspector - Sources - fetches
    url = "https://audioapi.orf.at/wie/json/4.0/live"
    response = http.get(url)
    alldata = response.json()
    # index number to find currently playing song
    arraynumber = 0 
    # searching which "magazine" (broadcast) currently is running - only relevant when magazines are switching
    for mag in alldata:
        # if magazine has not ended yet, take first json item [mag_item=0]
        if mag["end"] > metric:
            mag_item = 0
            #print(f"Magif: {mag_item}")
            break
        # stops the for loop and moves on (for some reason the loop goes on after the else: and overwrites mag_item with 0 again
        else:
            # if magazine has ended, take second json item [mag_item=1]
            mag_item = 1
            #print(f"Magelse: {mag_item}")
            break
        # stops the for loop and moves on (for some reason the loop goes on after the else: and overwrites mag_item with 0 again
    # title of magazine/broadcast
    sendung = response.json()[mag_item]['title']
    # items within magazine to find what's currently playing
    data = response.json()[mag_item]['items']
    for count in data:
        if count["end"] < metric:
            arraynumber = arraynumber + 1
    data2 = response.json()[mag_item]['items'][arraynumber]
    # data2 is last song within items array
    type = data2['type']
    end_t = data2["endISO"]
    end_time = end_t.split("T",1)[1][:8]
        # type gives what is playing: M=music; j=jingle; N=news; ...
    if type == "M": 
        current_song = data2["title"]
        artist = data2["interpreter"]
        image = data2['images'][0]['versions'][1]
        image_url = image["path"]
        img = http.get(image_url)
    else:
        current_song = data2['title']
        artist = "Radio Wien"
        image_url = "https://play-lh.googleusercontent.com/9sGSok5kd2h0ZOmppRhCAWJKh7B96AT_gyVDT7u3xOdrCNz3D9beI5k3kFiwuRc6G3U=w480-h960"
        img = http.get(image_url)
        
    albumWidget = render.Image(src = img.body(), height = 16, width = 16)
    return renderIt(now, albumWidget, current_song, sendung, artist, end_time)

def renderIt(now, albumWidget, current_song, sendung, artist, end_time):
    return render.Root(
child = render.Stack(
    children = [
        render.Padding(
            pad = (16, 1, 0, 0),
            child = render.Text(
                content = now.format("15:04"),
                font = "tom-thumb",
                color = "#fff",
            ),
        ),
        render.Padding(
            pad = (37, 1, 0, 0),
            child = render.Text(">>%s" % end_time.format("15:04"),
                font = "tom-thumb",
                color = "#777",
            ),
        ),
        render.Box(
            color = "#00FF0000",
            child = render.Padding(
                pad = (0, 3, 0, 0),
                color = "#FF000000",
                child = render.Column(
                    cross_align = "end",
                    main_align = "start",
                    expanded = False,
                    children = [
                        render.Box(
                            color = "#0000FF00",
                            height = 4,
                        ),
                        render.Marquee(
                            width=48,
                            child=render.Text("%s" % sendung, color = "#DC6F2E"),
                            offset_start=0,
                            offset_end=0,
                        ),
                        render.Box(
                            color = "#0000FF00",
                            height = 2,
                        ),
                        render.Marquee(
                            width=64,
                            child=render.Text("%s" % current_song, color = "#FFF"),
                        ),
                        render.Marquee(
                            width=64,
                            child=render.Text("%s" % artist, color = "#DF312F"),
                            offset_start=0,
                            offset_end=0,
                        ),
                    ],
                ),
            ),
        ),
        
        render.Padding(
            pad = (0, 0, 0, 0),
            child = albumWidget,
        ),
    ],
),
)

Picture: Album cover
First line: Start time of song >> End time of song
Second line: Name of radio show
Third line: Song title
Fourth line: Artist

1 Like

Thanks for writing this out. It’s really baffling that they still do not support local app development. This thread goes back to 2020: https://discuss.tidbyt.com/t/pixlet-push-doesnt-install-user-created-app/74

The whole point of this device is the DIY aspect. I know some people are just happy with the pre-available apps – and they’re great – but it’s marketed specifically to people who want to write apps.

I guess they have no plans of ever supporting local apps? How dissapointing.

Local Apps are supported and can be pushed as installations. What has to happen, though, is that it needs to be repushed as an installation every time you want to refresh the data from the API.

I’d also strongly consider publishing to the community repo.

The process is lightweight, means other people can benefit, and saves you having to worry about orchestration as tidbyt will run everything for you.