RPi Newtorked Digital Radio Recorder (timeshift & podcasts)


2 posts
by rob0 » Tue Nov 06, 2012 3:51 am
Here's a quick write up on using the Raspberry Pi as a networked headless server for time shifting internet radio streams and playing podcasts or other music through a stereo, using a single webpage as a remote control allowing you to pause, rewind, fast forward, volume changes, skip to prev / next track in a playlist etc.

Note this isnt that hard to do, but at this stage isnt a complete beginner plug & play solution. With that caveta....

Basic setup that I've used:
Raspbian “wheezy” (though not strictly required as long as you have the other parts)
Lighttpd (another light weight server supporting cgi should work as well)
podgrab.py (version 1.1.1 by Jonathan Baker, manages podcasts)
mplayer (using named pipes to control via webpage remote control)

What it does:
1) Downloads and manages a set of podcasts
2) Records radio programs for later time shifted playback
3) Will act as as the equivalent of a radio Tivo or DVR (well a Digital Radio Recorder) with the ability to pause live radio streams, rewind and fast forward (using a buffer).
4) Skip ahead / back 30s or 5 min (user adjustable)
5) Move through playlists of music or podcasts
6) Adjust volume and stop the stream
7) Can play back mp3 albums
8) All controlled via iPhone, Andriod or computer via webpage.

Part 1: Controlling mplayer

After setting up Lighttpd with cgi (I tried fastcgi, but haven't had luck with that - and for this setup cgi is working) I created a simple webpage with buttons for each of the commands, podcasts and internet radio streams that I have available to listen to. When you click on a button it fires a javascript function that takes 2 arguments: the type of data being sent to mplayer and the data itself.

The javascript function calls a python cgi script which forwards the data on to a named pipe which mplayer is listening to.

To simplify things I don't use XMLHttpRequest, instead I just do a simple img src call (similar to this idea: http://www.phpied.com/ajax-with-images/ )

Code: Select all
<!DOCTYPE html>
<html> <head> <title> RadioyPlayer </title>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta content="text/html; charset=iso-8859-1" http-equiv="Content-Type" />
<meta content="minimum-scale=1.0, width=device-width, maximum-scale=0.6667, user-scalable=no" name="viewport" />
<link rel="apple-touch-icon" href="DRR.png"/>
<link rel="apple-touch-startup-image" media="(max-device-width: 480px) and (-webkit-min-device-pixel-ratio: 2)" href="Splash.png" />

<script>function action(a,b){document.getElementById('fn').innerHTML='<img src="/cgi-bin/radioAction2.py?input1='+a+'&input2='+b+'" style="display:none;" />';}</script>

</head>
<body>
<h1 align="center">Selection Station:</h1>
<div align="center"><button style=width:55px;height:44px" onclick="action('c','seek -300 0')"><h2><<<</h2></button><button style=width:55px;height:44px" onclick="action('c','seek -30 0')"><h2><<</h2><button style=width:55px;height:44px" onclick="action('c','pause')"><h2>||</h2></button><button style=width:55px;height:44px" onclick="action('c','seek 30 0')"><h2>>></h2></button><button style=width:55px;height:44px" onclick="action('c','seek 300 0')"><h2>>>></h2></button></div>
<div align="center"><button style=width:52px;height:44px" onclick="action('c','pt_step -1')"><h2><|</h2></button></button><button style=width:53px;height:44px" onclick="action('c','quit')"><h2>X</h2></button><button style=width:52px;height:44px" onclick="action('c','pt_step 1')"><h2>|></h2></button><button style=width:60px;height:44px" onclick="action('c','volume <50')"><h2>Vol-</h2></button><button style=width:60px;height:44px" onclick="action('c','volume 50>')"><h2>Vol+</h2></button></div><br>
<div align="center"><button style="width:140px;height:44px" onclick="action('u','http://wbur-sc.streamguys.com/wbur.mp3')"><h2>WBUR</h2></button>&nbsp;&nbsp;&nbsp;<button style="width:140px;height:44px" onclick="action('u','http://streams.wgbh.org:8000')"><h2>WGBH</h2></button></div>
<div align="center"><button style="width:140px;height:44px" onclick="action('u','http://kqed-ice.streamguys.org:80/kqedradio-ch-e1')"><h2>KQED</h2></button>&nbsp;&nbsp;&nbsp;<button style="width:140px;height:44px" onclick="action('u','http://207.245.67.204:80/')"><h2>WHYY</h2></button></div>
<div align="center"><button style="width:140px;height:44px" onclick="action('u','http://sc5.lga.llnw.net:80/stream/wbez_91_5_fm')"><h2>WBEZ</h2></button>&nbsp;&nbsp;&nbsp;<button style="width:140px;height:44px" onclick="action('u','http://stream1.opb.org/kmhd.mp3')"><h2>KMHD</h2></button></div><br>
<div align="center"><button style="width:300px;height:44px" onclick="action('f','/home/pi/radio/morningEd.mp3')"><h2>Morning Edition</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('f','/home/pi/radio/ATC.mp3')"><h2>All Things Considered</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('f','/home/pi/radio/marketplace.mp3')"><h2>Latest Marketplace</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/APM-Marketplace/')"><h2>Marketplace Podcast</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/APM-Marketplace-Tech-Report/')"><h2>Marketplace Tech Report</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/APM-Marketplace-Money/')"><h2>Marketplace Money</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/NPR-Planet-Money-Podcast/')"><h2>Planet Money</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/NPR-Car-Talk-Podcast/')"><h2>Car Talk</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/NPR-Programs-Wait-Wait...-Dont-Tell-Me-Podcast/')"><h2>Wait Wait</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/WNYCs-Radiolab/')"><h2>Radiolab</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/On-the-Media/')"><h2>On the Media</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/Studio-360-from-PRI-and-WNYC/')"><h2>Studio 360</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/Science-Friday-Audio-Podcast/')"><h2>Science Friday</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/Says_You/')"><h2>Says You</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/My_Word/')"><h2>My Word</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/NPR-Ask-Me-Another-Podcast/')"><h2>Ask Me Another</h2></button><br></div>
<div align="center"><button style="width:300px;height:44px" onclick="action('d','/home/pi/podcasts/The-Moth-Podcast/')"><h2>The Moth</h2></button><br></div>
<div id="fn"></div>
</body></html>


Part 2: The python script
The python script is written so you can call it from the command line with arguements, OR as a cgi script. If the first argument will tell the script what type of command you're trying to execute which helps it decide if it needs to stop a previously running stream, or create a playlist from a directory, send a command, or just play a file.

Code: Select all
#!/usr/bin/env python
import subprocess
import argparse,os
import cgi

command,filename,directory,url=False,False,False,False

fs=cgi.FieldStorage()
input1=fs.getvalue("input1")
input2=fs.getvalue("input2")

if input1=='c':   command=input2
elif input1=='f': filename=input2
elif input1=='d': directory=input2
elif input1=='u': url=input2

#-c command
#-f single file to play
#-d a directory, there maybe multiple files, podcasts or music playlist directory
#-u streaming url

else:
   parser=argparse.ArgumentParser()
   group = parser.add_mutually_exclusive_group()
   group.add_argument("-c", dest="command", help="Issue commands like pause, stop, skip to the player")
   group.add_argument("-f", dest="filename", help="Provide a single file to listen to.")
   group.add_argument("-d", dest="directory", help="Provide a directory with 1 or more mp3s, music or podcasts.")
   group.add_argument("-u", dest="url", help="Provide a streaming URL to listen to.")
   args = parser.parse_args()
   if args.command: command=args.command
   if args.filename: filename=args.filename
   if args.directory: directory=args.directory
   if args.url: url=args.url

if command:
   #command issued, forward to mplayer
   ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE).communicate()[0]
   if "mplayer -slave" in ps:
      os.system('sudo echo '+command+' > /home/pi/mplayercontrol')
   

else:
   #this is done after checking if a command was issued - we want to only stop the playing
   #of a previous file IF we are told to play a new one, NOT if a command is issued

   ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE).communicate()[0]

   if "mplayer -slave" in ps:
           processes = ps.split('\n')
           nfields = len(processes[0].split())-1
           for row in processes[1:]:
                   if "mplayer -slave" in row:
                           id = row.split(None,nfields)
                           pid = id[1]
                           os.system("sudo kill -9 "+pid)

   if filename:
      #play a single file with mplayer
      ps = subprocess.Popen(["sudo","nohup","mplayer" ,"-slave", "-input", "file=/home/pi/mplayercontrol", filename],stdout=subprocess.PIPE,stderr=subprocess.PIPE)
   
   elif directory:
        #playing all the files in a directory so create a playlist file to send to mplayer
      DIR = directory
      os.system('sudo ls -r '+DIR+'*.mp3 >'+DIR+'list.txt')
      mp3s = DIR+'list.txt'

      ps = subprocess.Popen(["sudo","nohup","mplayer" ,"-slave", "-input", "file=/home/pi/mplayercontrol", "-playlist" ,mp3s],stdout=subprocess.PIPE,stderr=subprocess.PIPE)

   elif url:
      #streaming
      ps = subprocess.Popen(["sudo","nohup","mplayer" ,"-slave", "-input", "file=/home/pi/mplayercontrol", url],stdout=subprocess.PIPE,stderr=subprocess.PIPE)


   out,err=ps.communicate()
   print "out = "+out
   print "err = "+err
   
   #now set the staring volume to 90%
   os.system('sudo echo "volume 90 1" > /home/pi/mplayercontrol')
   

print 'Status: 204 No response'
print


Part 3: podgrab.py script to manage podcasts
Using podgrab or a similar command line script to easily manage the downloading of podcasts. I use cron to check for new podcasts and save 1 month of podcasts.

Part 4: scripts to save radio streams to file;
Here's an example script to save marketplace from a radio stream - I do this since the podcast is sometimes posted hours after it's broadcast over the internet.

Code: Select all
#!/bin/bash
mplayer http://streams.wgbh.org:8000 -dumpstream -dumpfile /home/pi/radio/marketplace.mp3 &
PID_mytask=$!
sleep 30m
kill -9 $PID_mytask


I use cron to start this script at the appropriate time - it will run for 30 minutes and kill itself. It's also designed to overwrite itself each time.

Note - for this to work you have to make sure the www-data user can write to the directory where you store your podcasts or music since it wants to save a playlist file to that directory, you also need to setup your named pipe correctly (using mkfifo) as you can see mine is called mplayercontrol in the scripts above.

Hopefully this is useful for anyone else looking to do something similar.

-Rob0
Posts: 4
Joined: Mon Nov 05, 2012 4:49 am
by anewcomb » Thu Dec 20, 2012 7:33 pm
Awesome! Thanks for posting this. I am looking to do something very similar and this was helpful.
Posts: 1
Joined: Thu Dec 20, 2012 7:31 pm