Ever since I got my Raspberry Pi I wanted to write a simple web application for it. My requirement was to use Python as programming language and some interaction that lets Mr. Pi actually do something I could really experience by my senses.
A decided to go for a music application that I could control from my mobile phone. The implementation is of course mainly for educational purposes. For instance mpd can be used on linux systems with sophisticated web clients to give much broader functionality.
Nevertheless, we combine Flask, SQLite, Tornado and Bootstrap in a simple application! This approach can act as a basic blueprint for much more sophisticated Python web applications based on database queries. Ok, let's get started!
The Jinja2 template for Flask
To use Flask, we need to set up a template in a folder '/templates'. This template is later filled by Flask with values from a dictionary. The template is designed to have no local css, js or image files attached to it - all necessary files are loaded from globally available content delivery networks. In this way we can also simply load the html file with a browser to see if it is working with respect to some Jinja placeholders in curly brackets.
Here is the file that you may want to save as 'main_bootstrap.html' in the '/templates' folder:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="description" content="Simple interface to make some music using a raspberry pi">
<meta name="author" content="Robert Filter">
<link rel="icon" href="">
<title>{{ title }}</title>
<!-- Bootstrap core CSS -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{ title }}</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Home</a></li>
<li><a href="#sl">List of Streams</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div class="container-fluid">
<div class="jumbotron">
<h1>My awesome Jukebox</h1>
<div class="btn-group btn-group-justified">
<a href="/stop" class="btn btn-lg btn-warning">Stop</a>
<a href="/shutdown_server" class="btn btn-lg btn-danger">Shutdown Jukebox</a>
<a href="/shutdown" class="btn btn-lg btn-danger">Shutdown Pi</a>
</div>
</div>
<div id="sl">
{% for entry in entries %}
<div class="well">
<h2>{{ entry.name }}</h2>
<div class="row">
<div class="col-md-3">
<div></div>
</div>
<div class="col-md-3">
<div>Genre: <b>{{ entry.genre }}</b></div>
</div>
<div class="col-md-3">
<div>Rating: <b>{{ entry.rating }}</b></div>
</div>
<div class="col-md-3">
<div class="pull-right">
<a class="btn btn-info btn-large" href="/{{ entry.id }}">PLAY</a>
</div>
</div>
</div>
</div>
{% else %}
<li><em>Unbelievable. No entries so far</em>
{% endfor %}
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
</body>
</html>
Maybe some short explanations about the actual html structure of the code. In the head-section at the beginning you can see how the bootstrap css are loaded from a content delivery network. This is also the case for the js files loaded at the end of the site. Note that if you want to use this small file as a starting point for an offline application, you have to use static css and js files. A further sidenote on the html code: in Bootstrap 4 the class 'well' is obsolete and replaced by 'cards'.
Soon after the head section these is a simple navbar that has very little meaning for our one-page application but may be important to modify for multipage applications. In the jumbotron you can find the first actual links to other pages: /stop, /shutdown_server, and /shutdown. We will use these links to instruct Flask to perform certain tasks: stop the music, shutdown the web server (our jukebox) and shutdown the computer itself, see the @app.route('/somelink')-functions in the Python code below.
The main important parts of the template are inside the div id 'sl', which is a very bad abbreviation for 'List of Streams'... In this div we can find a loop through entries, which is a list of dictionaries. Any part of the list has the elements 'name', 'genre', 'rating', and 'id'. These values are loaded by our Python code from an SQLite database and rendered by Jinja into the correct positions of the html. How? Let's introduce the main Python program.
The Python code to run the Flask app, a Tornado server and manage the SQLite database
You may save the following file under any name you like. I was creative and used play_music.py. The comments inside the code should explain the main building blocks.
# Some necessary includes: Flask, SQLite and some system functions
from flask import Flask, g, render_template, redirect, request
import sqlite3 as lite
import sys, os
from subprocess import *
# Tornado web server
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop
# Database name. You may use any other database name you like.
DATABASE = '/home/pi/mp3/streams.db'
# Just a debugging flag to switch off Flask and Tornado
web = True
# Clever people put the database handling into separate files...
# Anyways, the first function simply creates the SQLite database
# Please change/add the music streams to your liking.
def create_whole_db(DATABASE):
table_data = {
'streams':(
(1,'Lounge FM', r'http://stream.lounge.fm/', 'Lounge', 5),
(2,'MDR Info',r'http://c22033-ls.i.core.cdn.streamfarm.net/QpZptC4ta9922033/22033mdr/live/app2128740352/w2128904192/live_de_128.mp3', 'Info', 3),
(3,'Klassik Radio',r'http://edge.live.mp3.mdn.newmedia.nacamar.net/klassikradio128/livestream.mp3', 'Klassik', 2),
)}
#connect to database
db_connection = lite.connect(DATABASE)
cursor = db_connection.cursor()
#create table
cursor.execute("DROP TABLE IF EXISTS Streams")
cursor.execute("CREATE TABLE Streams" +\
"(id INT, name VARCHAR(255), link VARCHAR(255), genre VARCHAR(255), rating INT)")
for data in table_data['streams']:
qstr = "INSERT INTO Streams " +\
"(id, name, link, genre, rating) values ('%d', '%s', '%s', '%s', '%d')" %(data[0], data[1], data[2], data[3], data[4])
print qstr
cursor.execute(qstr)
#necessary - at least in windows...
db_connection.commit()
db_connection.close()
# This function reads out the table streams from the database.
# It returns a list of dictionaries Flask and Jinja can process.
def return_dict(DATABASE):
db_connection = lite.connect(DATABASE)
with db_connection:
cursor = db_connection.cursor()
data = cursor.execute("SELECT id, name, link, genre, rating FROM Streams")
rows = data.fetchall()
#create list of dictionaries
dict_here = [dict(id=row[0], name=row[1], link=row[2], genre=row[3], rating=row[4]) for row in rows]
return dict_here
# To execute commands outside of Python
def run_cmd(cmd):
p = Popen(cmd, shell=True, stdout=PIPE, stderr=STDOUT)
output = p.communicate()[0]
return output
# Initialize Flask.
if web:
app = Flask(__name__)
if web:
@app.route('/')
def show_entries():
general_Data = {
'title' : 'Jukebox v0.1'}
stream_entries = return_dict(DATABASE)
return render_template('main_bootstrap.html', entries = stream_entries, **general_Data)
# We do stop the music...
@app.route('/stop')
def stop_music():
run_cmd('mpc stop')
return redirect('/')
# Play a stream from the id provided in the html string.
# We use mpc as actual program to handle the mp3 streams.
@app.route('/<int:stream_id>')
def mpc_play(stream_id):
db_connection = lite.connect(DATABASE)
with db_connection:
cursor = db_connection.cursor()
data = cursor.execute("SELECT link FROM Streams WHERE id='%d'" % (stream_id) )
link_here = data.fetchone()[0]
run_cmd('mpc clear')
run_cmd( ['mpc add %s' % (link_here)])
print link_here
run_cmd('mpc play')
return redirect('/')
# Shutdown the computer.
@app.route('/shutdown')
def shutdown_now():
run_cmd('sudo halt')
return 'Goodbye'
# To gracefully shutdown the web application.
@app.route('/shutdown_server', methods=['POST', 'GET'])
def shutdown():
IOLoop.instance().stop()
return 'Shutting down the server.\nSee you soon :)'
# Here comes the main call.
# See how simple it is to launch a Tornado server with HTTPServer.
if __name__ == "__main__":
create_whole_db(DATABASE)
if web:
http_server = HTTPServer(WSGIContainer(app))
http_server.listen(8080)
IOLoop.instance().start()
If you have saved everything accordingly, you may start the program by
python play_music.py
The web interface should be available in your browser at the ip of your remote linux computer (port 8080), e.g. 192.168.1.2:8080. Here is how it should look like now:
The most important test is now to click on one of the play buttons. Hopefully the streams are still alive and some music comes out of the speakers attached to the remote computer.
Launching the Flask Jukebox via SSH
If you would like to launch the jukebox via SSH, the application will unfortunately break after you have disconnect because it receives a hangup (HUP) signal. To prevent this, you can use the program nohup. Save the following script to play.sh
1 2 |
|
After you have told linux that the script is executable (chmod +x play.sh), you can run the Flasky jukebox by ./play.sh.
Conclusions
We have seen how to build a simple jukebox application based on Python and mpc. The main Python libraries we used were Flask, SQLite and Tornado. Furthermore, a simplified Bootstrap template was provided to enable a responsive design of the application. The Flask-SQLite-Tornado-Bootstrap jukebox is an educational approach to combine all involved exciting technologies. I hope you could make some use of the implementation. Thank you for reading!