A Jukebox in sh with netcat

Published 2018-12-17

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