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.
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?
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+
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.)
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.
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
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 :)