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

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