A Jukebox in sh with netcat
Over winter break, some friends and I decided to spend a week working on a programming project. One challenge was playing music - deciding whose computer to use, which person’s music library to use, and so on. To make it easier for everyone to play the tracks they wanted, we set up a spare laptop as a jukebox.
A simple shell script which we’ll discuss below served as a server - people
could send it a URL (such as a YouTube video) via netcat and it would
automatically play each link using mpv
one at a time in a
first-come-first-serve order.
This approach is definitely not the most robust - for one thing anyone could easily overload the server, skip other people’s tracks, or cause the server to exit. It makes the assumption that nobody on your network is a bad actor. But, for a temporary setup on a secure LAN, it served us well.
How it Works
The first script, muserver.sh
, uses netcat to listen for incoming
connections, parse out either a command or a URL, then take the appropriate
action. In the case where the data the client sent was a URL, it gets sent into
a named pipe for the music playback script to pick up.
The second script, muplayer.sh
, reads the named pipe from the former one line
at a time. It tries to play each line using mpv
(which uses youtube-dl
under the hood to retrieve the music files). It waits until mpv
exists before
retrieving the next line.
Using the Server
On the server - the machine which you want the music to play out of, you just
need to run muserver.sh path/to/fifo
, where path/to/fifo
is where you would
like the named pipe to be placed. The server should print the IP and port
number which it is using.
Now, anyone who can access your IP can use netcat to send URLs to be played
like so: echo 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' | nc the_ip the_port
.
If you want to skip a track: echo 'SKIP' | nc the_ip the_port
When you’re ready to close the server: echo 'DIE' | nc the_ip the_port
muserver.sh
#!/bin/sh
# the user needs to tell us where to put the FIFO
if [ $# -ne 1 ] ; then
echo "$0 [FIFO path]"
exit 1
fi
FIFO="$1"
# get the current LAN IP via get-ip:
# https://git.sr.ht/%7Echarles/charles-util/tree/master/bin/get-ip
IP="$(get-ip)"
# set a port for netcat to use
PORT=5656
# create the named pipe
rm -f "$FIFO"
mkfifo "$FIFO"
# Keep the FIFO open - when we use 'echo >> thefifo' an EOF gets sent into the
# FIFO implicitly, this causes it to close unless another process has an
# open write handle; we spawn a background process that keeps a write handle
# open but does not actually write any data to it.
(while true ; do sleep 1 ; done > "$FIFO") &
# we want to keep track of the PID of the background process so we can kill
# it when we exit cleanly
SLEEPER_PID=$!
echo "sleeper PID=$SLEEPER_PID"
# run the music player script in the background
./muplayer.sh "$FIFO" &
# we want to keep track of the PID of the player so we can kill and restart
# it for skips, and also when we exit cleanly
PLAYER_PID=$!
echo "player PID=$PLAYER_PID"
echo "starting muserver... "
echo "my IP is $IP"
echo "listening on port $PORT"
while true ; do
# wait until a client sends us a message
RECV="$(nc -l $PORT)"
echo "muserver: got message '$RECV'"
# if the client asked us to skip, kick over the music player and
# restart it - we also kill mpv, since for some reason killing the
# parent does not also kill mpv.
if [ "$RECV" = "SKIP" ] ; then
echo "muserver: skip requested, kicking over player ($PLAYER_PID)"
kill -9 $PLAYER_PID
pkill mpv
./muplayer.sh "$FIFO" &
PLAYER_PID=$!
echo "muserver: player PID=$PLAYER_PID"
# if the client asks us to exit, kill all of the background proceses
# and exit
elif [ "$RECV" = "DIE" ] ; then
echo "muserver: clean shutdown"
kill -9 $PLAYER_PID
kill -9 $SLEEPER_PID
pkill mpv
exit
# assume the message is a link foor us to play and send it into the
# FIFO
else
echo "muserver: appended to queue"
echo "$RECV" >> "$FIFO"
fi
done
muplayer.sh
#!/bin/sh
# make sure we were passed in a FIFO to read from
if [ $# -ne 1 ] ; then
echo "$0 [FIFO path]"
exit 1
fi
FIFO="$1"
if [ ! -e "$FIFO" ] ; then
echo "$FIFO does not exist"
exit 1
fi
# read a line at a time from the FIFO
while read -r line ; do
echo "muplayer: begin playing '$line'"
# wait until mpv exits to proceed
mpv --no-video "$line" --quiet | while read -r ol ; do echo "mpv: $ol" ; done
echo "muplayer: finished playing (rc=$?)"
done < "$FIFO"