Shivang Nagaria

Learning the Life!

Pi Radio

Reading Time: 5 Min
Posted at — Apr 18, 2026

I like listening to music while working or doing chores around the house. Spotify and YouTube Music are fine — I use them most of the time — but they have a problem. They get too personalized. After a while, they’re just playing the same songs again & again. Sometimes you don’t want curation. Sometimes you just want to lose control a little.

A few months back, a friend sold me a Raspberry Pi 4. Around the same time I read that you can stream live radio via internet. The idea clicked: connect my JBL speaker to the Pi over Bluetooth, stream internet radio, and control it from my phone. Simple enough that it felt doable on weekends.

Over a few weeks, I built it. And the best part — you’d never guess what I used for the server.

The false start

My first attempt was a tool called tera — a bash script for streaming internet radio. It worked, but it had no daemon mode and no way to control it remotely without SSH-ing in every time. Not what I wanted.

Out of curiosity I read through its source. Turns out tera is mostly a wrapper: it stores stream URLs in a JSON file and passes them to mpv. That’s it. I didn’t need tera at all. I just needed mpv and a list of URLs.

Radio India|http://stream.radioindiauk.com/...
BBC World Service|http://stream.live.vc.bbcmedia.co.uk/...

mpv pointed at a stream URL, running in the background — that’s the whole radio player. The only open question was: how do I control it without typing into a terminal?

The server is 10 lines of bash

I didn’t want a full-blown server. Installing Nginx or Node for a start/stop toggle felt like using a sword to kill a fly. So I looked for the dumbest possible solution.

I found nc — netcat. It opens a port, writes input to a file and echoes back the response. That is essentially what a server does.

while true; do
    printf "HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 0\r\n\r\n" \
        | nc -l 0.0.0.0 1234 -N > /tmp/nc_input

    grep -q "start" /tmp/nc_input && {
        bluetoothctl connect "$SPEAKER_MAC"
        mpv -ao=pulse "$STREAM_URL" > /tmp/mpv.log 2>&1 &
        echo $! > /tmp/mpv.pid
    }

    grep -q "stop" /tmp/nc_input && kill $(cat /tmp/mpv.pid)
    grep -q "next" /tmp/nc_input && { kill_mpv; next_station; start_mpv; }
    grep -q "vol+" /tmp/nc_input && pactl set-sink-volume @DEFAULT_SINK@ +10%
done

Inside the loop: nc listens on port 1234, returns a 200 OK immediately (so the browser doesn’t hang), dumps the raw HTTP request to a file, then I grep the file for the command. That’s the whole thing. No routes, no middleware, no framework.

And, here is the magic - from any device on the home network I can do following:

To start the FM

curl 192.168.0.207:1234/start (or just drop this url in mobile browser)

To move to next stations

curl 192.168.0.207:1234/next

To increase the volume

curl 192.168.0.207:1234/vol+

Few hiccups

A script you have to manually run after every reboot isn’t very usable. So I wired it up to systemd.

I also ran into a few weird issues. Running the service as root caused audio to not route correctly — Bluetooth and PulseAudio both live in user-space, so dropping to a regular user fixed it. The script also needs to call pulseaudio --start before doing anything else, otherwise the service comes up before the audio system is ready. And the bass sounded terrible until I realized pulseaudio-equalizer was missing. Installed it, rebooted, speaker finally started sounding okay.

(journalctl -fu <service-name> is your friend for debugging all of this.)

Adding a UI

After using my phone’s browser for a while, I built a small browser UI. No frameworks — just HTML, CSS, and fetch calls hitting the same nc server.

A background watcher process writes a status.json every few seconds by polling mpv, reading stream metadata from its log, and querying Bluetooth and PulseAudio state:

{"ble":true,"ble_name":"Flip 5","playing":true,"song":"Durandhar - Gehra Hua","station":"Radio India","vol":57}

The frontend polls that file and updates accordingly.

The annoying bits

A few things that cost me more time than they should have.

I gave the Pi a static IP (192.168.0.207) so the curl URL never changes — a few lines in /etc/network/interfaces handles this.

I travel more than I wanted to and I wanted my Pi to work on all locations. It’s a bitch to setup wifi config on Pi. If by any means, if you forget what wifi/password you used - you have to setup Pi from the scratch again.

So what I did was two things

sudo nmcli con mod "home_wifi" connection.autoconnect-priority 1 # Home wifi, not going anywhere
sudo nmcli con mod "mobile_hotspot" connection.autoconnect-priority 2 # mobile hotspot, moves with me

That is it

The whole thing — nc server, Bluetooth management, volume control, the watcher — is about 180 lines of bash. It runs headlessly, survives reboots, and I’ve been enjoying it.

The part I keep thinking about is the nc trick. I really feel proud that I was able to make this work without any heavy webserver. Just using first principle thinking got me this far.

You can find the source code here: github.com/shivtej1505/pi-radio


Read next

Creating FileSystem from scratch

I was reading a chapter on persistence from a book called OSTEP (a classic OS book by Prof. Remzi). It has all kinds of crazy things that I don’t hear about every day: storage devices, drivers, pages, virtualization of disk, and many other things.


Why poll when you can pull?

Get new posts delivered to your inbox.

Liked what you are reading? Let me know here me@shivangnagaria.com :)