Introduction: Raspberry Pi Internet Radio With Flask

Picture of Raspberry Pi Internet Radio With Flask

The purpose of this tutorial is to show you how you can build a radio that can stream audio from the internet using a simple web interface to operate it. We will need the following:

  • Raspberry Pi
  • Some speakers you can hook up to your Raspberry Pi
  • Flask (Python framework)
  • Router with internet access
  • UTP cable or WiFi dongle to let the Raspberry Pi connect to your network
  • Computer with network access
  • A SSH client like PuTTY or Tera Term
  • A FTP client like FileZilla

This tutorial will not show you how to put an OS on the Raspberry Pi, how use the CLI or how to open a SSH connection. There are plenty of great tutorials for those already.

Step 1: Setup

Connect your Raspberry Pi to the internet and update the software libraries to make sure you get the latest versions of the software we need.

sudo apt-get update

Then install the Music Player Daemon and Music Player Command, two really great software programs to get music playing.

sudo apt-get install mpd mpc

If you want to Raspberry to stream its audio to the analogue 3.5mm audio jack, use this command and set it to 1. To set it to HDMI, use 2. The default is 0, which is automatic.

amixer cset numid=3 1

Next we need to install some Python tools. Flask is a framework for Python to develop website applications, which we will use to build a simple user interface. Flask uses Jinja2 templates and the Werkzeug WSGI toolkit.

You can follow instructions on the Flask website (http://flask.pocoo.org/) for installation, or follow the steps I took below.

Try with easy_install tools,

sudo apt-get install python-pip 
sudo easy_install-pypy flask

Or with pip.

sudo apt-get install python-pip 
sudo pip install flask

if you are also using an Apache webserver to also host your website, read this:

Flask Apache2 mod_wsgi

Step 2: Test Mpd & Mpc

Next we will use some commands in mpc to make mpd do its magic.

To view a useful list of commands, use this.

mpc help

Add a music stream to mpc.

mpc add http://radio.nolife-radio.com:9000/stream

And use these simple commands to play and stop

mpc play
mpc stop

Your Raspberry Pi should now be playing some tunes from an internet radio service.

If it doesnt, clear and try with a different url.

mpc clear

Step 3: Jinja2 Template

Picture of Jinja2 Template

Now comes the part to build the interface. The reason we use Flask is so we can instantiate the commands from a remote web client (like the ones above) using Python.

Create a folder to store your files in. Inside that folder, create a folder named “templates”. Here we will make a html template file that will be interpreted by Jinja2.

Interface.html

<!doctype html>
<head>
<title>RPi Radio</title>
</head>
<body>
{% if name %}
	<h1>Welcome to {{ name }}</h1>
{% else %}
	<h1>Welcome to RPi Radio</h1>
{% endif %}
<form role="form" method='POST' action='/'>
	<input type="submit" name="submit" value="turn radio on" />
	<input type="submit" name="submit" value="turn radio off" />
</form>
</body>

This is a simple html file with a variable and two buttons.

Step 4: Python Code

Picture of Python Code

Next comes the python code. Put it in a file named radio.py

from flask import Flask
from flask import render_template
from flask import request
import subprocess

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])

def hello_world(name='Flask FM'):
	if request.method == 'POST':
		if request.form['submit'] == 'turn radio on':
			cmd=['mpc', 'play']
			p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
		elif request.form['submit'] == 'turn radio off':
			cmd=['mpc', 'stop']
			p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)

	return render_template('interface.html', name=name)

if __name__ == '__main__':
	app.run(host='0.0.0.0', port=1234, debug=True)

First make the necessary imports for the processes we will need. Then we define a function with a parameter called name. We instantiate this variable with the string ‘Flask FM’. In the Jinja2 HTML template, the variable {{ name }} will be filled with this string. This is just to use as an example for later. The hello_world() function will be called once this python code runs. If a POST request is being made, we need to send a command to the CLI.

Remember that Python is indentation sensitive, so when copy and pasting above code, make sure to get the tabs right.

<form role="form" method='POST' action='/'>
	<input type="submit" name="submit" value="turn radio on" />

In the Jinja2 template we defined that the form should make POST requests. The button name “submit” corresponds with the request.form['submit'] in our python code. The value of the button, which is also its button text, is the value we check for.

if request.form['submit'] == 'turn radio on':

If that button is pressed, a subprocess will be executed.

cmd=['mpc', 'play']
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)

Which will play our music stream.

Back to the CLI, run the python file.

sudo python radio.py

And navigate to the IP or domain name of your Raspberry Pi on port 1234.

You should see your Jinja2 template with the name variable and 2 buttons that will turn your radio on and off.

Step 5: Separating Credentials From Main File

If you want to use version control like git to keep track of your files, you should separate the sensitive data from the file you put online. You can simply make a new python file. Name this file predefines.py and place it in the same folder as radio.py.

We are also going to clean up some code by defining some functions.

host = '0.0.0.0'
port = 1234
txtFile = 'stations.txt'
templateFile = 'interface.html'
def isInteger(s):
	try: 
		int(s)
		return True
	except ValueError:
		return False

def mpcCommand(cmd):
	p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
	return	p.stdout.read()

And at the top of your radio.py file, add:

from predefines import host, port, txtFile, templateFile
from flask_apscheduler import APScheduler

To allow python to correctly display UTF-8 text (the radio stream may sometimes use UTF-8 characters in their titles), at the very top of the radio.py file, add the following 2 lines:

#!/usr/bin/python
# -*- coding: utf-8 -*-

Now all the redundant bits of code should now be replaced with the new mpcCommand() function. It will make the code a bit more readable and easier to add new commands.

For example, the following code:

if request.form['submit'] == 'turn radio on':
	cmd=['mpc', 'play']
	p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
	out,err = p.communicate()

Is replaced with:

if request.form['submit'] == 'turn radio on':
	mpcCommand(['mpc', 'play'])

Step 6: Adding Radio Stations

To make it a little more convenient, we will let Python parse a txt file where we define our radio stations. We separate the names from the URL by a ‘|’ delimiter. Here are some example stations that are currently active in 2016:

NPO Radio 1 NL | http://icecast.omroep.nl/radio1-sb-mp3
Rainwave Chiptune | http://icecast.omroep.nl/radio1-sb-mp3
Ibiza Global Radio | http://icecast.omroep.nl/radio1-sb-mp3
No-Life Radio | http://icecast.omroep.nl/radio1-sb-mp3
Smooth Jazz Florida | http://icecast.omroep.nl/radio1-sb-mp3
FM Odawara | mms://simul.freebit.net/fmodawara

You can also find many more on https://www.internet-radio.com

To parse this txt file, add the following code in the hello_world() function:

stations = []
stationURLs = []
stationOutput = ''

for x in open('stations.txt','r'):
	a = x.split("|") 
	stations.append(a[0]) 
	stationURLs.append(a[1].strip())

then after the if-else statement:

position = mpcCommand(['mpc', '-f', '%position'])
idx = position.split('[')
position = idx[0].strip()

if isInteger(position) == False:
	position = 0
x = 1
for station in stations:
	stationOutput += '<option value="' = str(x) + '" '
	if x == int(position):
		stationOutput += 'selected="selected"'
	stationOutput += '>' + station + '</option>'
	x += 1

This code basically fills 2 arrays with strings. The stationOutput variable is to build html code we will use in the front-end. Also, if a station is currently playing, we get its position. We will make this correspond with our dropdown, so when you navigate to the front end, the station that is playing at the moment is selected by default.

Then add another statement to the if-else structure. It picks the index of the selection box, which is an integer.

elif request.form['submit'] == 'change':
	mpcCommand(['mpc', 'play', str(request.form['station'])])

Finally update the return statement.

return render_template('interface.html', name=name, stations=stationOutput.strip())

We will also need to be able to update our playlist. Add this code to the if-else statement.

elif request.form['submit'] == 'update playlist':
	mpcCommand(['mpc', 'clear'])
	for stationURL in stationURLs:
		mpcCommand(['mpc', 'add', stationURL])

In the template file, add the following code to add a selection box where we can choose the radio station, along with a button. Add |safe next to the variable to allow it to insert html. Also add the update button. Make sure to add this code within the form tags.

<select name='station'>{{ stations|safe }}</select>
<input type="submit" name="submit" value="change" />
<br/>
<input type="submit" name="submit" value="update playlist" />

Step 7: Volume Buttons

Picture of Volume Buttons

First add the code to the if-else structure, add a command to get the actual volume level and update the return statement.

elif request.form['submit'] == '+5':
	mpcCommand(['mpc', 'volume', '+5'])
elif request.form['submit'] == '-5':
       mpcCommand(['mpc', 'volume', '-5'])
volume = mpcCommand(['mpc', 'volume'])

return render_template(templateFile, name=name, stations=stationOutput.strip(), status=status, volume=volume)

And to the Jinja2 template:

<hr/>
<input type="submit" name="submit" value="-5" /> {{ volume }}
<input type="submit" name="submit" value="+5" />
<hr/>

Step 8: Add a Startup Script

The Python script needs to be run on startup so we do not need to instantiate it every time. We will make a shell script and let crontab run it on reboot. To do this, first make the shell script.

create a new file inside the folder along with radio.py and predefines.py and name it launcher.sh and add this code:

cd /var/www/flask_dev
sudo python radio.py

cd (change directory) will navigate to the folder with your radio.py file in it. I placed mine under /var/www/, but you can place yours in home directory if you want. Just make sure to navigate to it.

Then execute the file as root using sudo.

Now your Raspberry Pi needs to run this little script on startup. First it has to be made executable.

chmod 755 radio.py

The 7, 5, 5 are the permissions for the "owner", "group" and "all users" groups respectively. These numbers are a combination of the numbers 4, 2, 1 and 0.

  • 4 is "read"
  • 2 is "write"
  • 1 is "execute"
  • 0 is "no permission"

so 7 is 4+2+1 for the owner, meaning it has read, write and execute access. the group and all users have no write permissions.

Now we will add this script to crontab.

sudo crontab -e

And enter the following:

@reboot sh /var/www/flask_dev/launcher.sh

This will add your script to crontab, and it will execute it upon each system reboot.

Test it out and see if it all works:

sudo reboot

And that should give you the bread and butter of a simple internet radio! There are plenty of things you can improve and expand upon of course. Im new to Flask, so my code may not have been as efficient as it could have been.

So any criticism and suggestions, please leave them in the comments below. :)

Thanks for reading!

Step 9: All Code

Jinja2 html template:

interface.html

<!doctype html>
<head>
<title>RPi Radio</title>
</head>
<body>
{% if name %}
	<h1>Welcome, {{ name }}</h1>
{% else %}
	<h1>Welcome to RPi Radio</h1>
{% endif %}

<form method='POST' action='/'>
	<input type="submit" name="submit" value="turn radio on" />
	<input type="submit" name="submit" value="turn radio off" />
	<br/>
	<select name='station'>{{ stations|safe }}</select>
	<input type="submit" name="submit" value="change" />
	<br/>
	<input type="submit" name="submit" value="update playlist" />
	<br/>
	<hr/>
	<input type="submit" name="submit" value="-5" /> {{ volume }}
	<input type="submit" name="submit" value="+5" />
	<hr/>
</form>
</body>

Python loose file:

predefines.py

import subprocess
host = '0.0.0.0'
port = 1234
txtFile = 'stations.txt'
templateFile = 'interface.html'

def isInteger(s):
	try: 
		int(s)
		return True
	except ValueError:
		return False
        
def mpcCommand(cmd):
	p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
	return p.stdout.read()

Python Flask code:

radio.py

#!/usr/bin/python
# -*- coding: utf-8 -*-

from flask import Flask
from flask import render_template
from flask import request
from predefines import host, port, txtFile, templateFile
from predefines import isInteger, mpcCommand
import subprocess

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])    
def hello_world(name='Flask FM'):
	stations = []
	stationURLs = []
	stationOutput = ''
            
	if request.method == 'POST':
		if request.form['submit'] == 'turn radio on':
			mpcCommand(['mpc', 'play'])
		elif request.form['submit'] == 'turn radio off':
			mpcCommand(['mpc', 'stop'])
        	elif request.form['submit'] == 'change':
			mpcCommand(['mpc', 'play', str(request.form['station'])])
        	elif request.form['submit'] == '+5':
			mpcCommand(['mpc', 'volume', '+5'])
        	elif request.form['submit'] == '-5':
			mpcCommand(['mpc', 'volume', '-5'])
        	elif request.form['submit'] == 'update playlist':
			mpcCommand(['mpc', 'clear'])
			for stationURL in stationURLs:
				mpcCommand(['mpc', 'add', stationURL])
            
        
	cmd=['mpc', '-f', '%position%']
	p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
	position = p.stdout.read()
	idx = position.split('[')
	position = idx[0].strip()

	if isInteger(position) == False:
		position = 0
	x = 1
	for station in stations:
		stationOutput += '<option value="' + str(x) + '" '
		if x == int(position):
			stationOutput += 'selected="selected"'
		stationOutput += '>' + station + '</option>'
		x += 1            
    
	volume = mpcCommand(['mpc', 'volume'])
    
    
	return render_template(templateFile, name=name, stations=stationOutput.strip(), volume=volume)    

if __name__ == '__main__': 
    app.run(host=host, port=port, debug=True)	

Comments

RajarajanR (author)2016-05-01

Thanks for the tutorial - but there are some copy paste errors in your code. For example the extra <br> showing up in the radio.py code. Also there is a sudden change in indentation near the cmd=['mpc', '-f' .. line. Because of this python throws an error saying 'return' outside function. Don't know if some lines are missing. Would it be possible for you to post a link to the original source code.

ruudje (author)RajarajanR2016-05-01

Thank you for your feedback.

as of now, the Instructables edit makes adding code a real hassle, it sometimes adds those <br> tags.

I have fixed the errors you pointed out, thanks!

RajarajanR (author)ruudje2016-05-01

Yes, that works. My radio (that I made about 2 years ago on the pi) has a 2 line LCD display with a small joystick on the top that I use to traverse the station/menus. Now with your flash idea I can jumpstart my next enhancement to do whatever I did with the physical switch by using a browser. Perhaps a bootstrap driven UI would be ideal to have the page adapt itself to mobile devices vs desktop browsers.

ruudje (author)RajarajanR2016-05-01

That looks awesome, thanks for posting a picture!

Nice that this tutorial has helped you out with your project RajarajaR, I hope it turns out as you want it. Indeed bootstrap would make the front-end look much better, as it does with all web pages.

btw, I tried adding the files with source code to the tutorial, but the editor does not let me on multiple browsers... I sent an email to someone on instructables about it.

fmaida (author)2016-04-18

Thank you very much for this tutorial, it was really interesting.

wold630 (author)2016-04-08

Thanks for sharing all this info!