The Need
I really wanted a web accessible interface to control streaming music on my media playing computer that is connected to my home stereo. With a mix of Last.fm, shell-fm, pyjamas, web.py, and a few hundred lines of python code, I succeeded adequately and thought I'd share my experience.
The Why
When I wanted to stream music on my media machine, I would to get on the command line, ssh to the machine with X11 forwarding, and run the last.fm linux client on the machine. This would display a sluggish last.fm client on whichever computer I was using to connect to the media machine.
But what if I'm switching computers or I want to use my Nokia N810 or Neo Freerunner to control the music? A web interface solves these issues.
NOTE: this "how to" assumes that you are familiar with compiling software from source on a Linux system.
Step 1
Sign up for a Last.fm account. It is a fairly straight forward process.
Step 2
Install shell-fm (follow the directions on the shell-fm webpage). Shell-fm may be in your repos, but the version may not be new enough for this to work.
edit your ~/.shell-fm/shell-fm.rc file to look like the following:
password = YOUR_LASTFM_PASSWORD
default-radio = lastfm://user/YOUR_LASTFM_USERNAME
unix = /home/YOUR_USER/.shell-fm/unix_file
daemon = true
What the configuration does:
- username and password should be self explanatory
- default-radio is used to set what starts streaming when shell-fm is started
- unix creates a unix file socket that allows for communication with shell-fm. My file is /home/jezra/.shell-fm/unix_file. we need to use this later
- daemon runs shell-fm as a daemon in the background
OK, that takes care of the player. Now onto the web server
Step 3
In order to get a web interface, I need some sort of webserver. There are many many different ways to create a webserver and for this example I chose to use web.py which is a damn simple web app framework for Python. If web.py isn't in your repos, you should switch distributions (or download the source).
My Layout of Files and directories
/testing_directory
--index.py #the webserver
--interface.py # the pyjamas interface for the web app
--templates/ #a directory containing one template that the webserver will use
--static/ #a directory of static content served by the web app
----styles/ #a directory of css styles used by the web app
----pyjamas/ #a directory containing the compiled output of the interface.py file
Lets move on to the files
index.py
import web
import socket
#define some variable
#define our url mapping
urls = (
'/', 'index',
'/track_info','get_track_info',
'/skip','skip',
'/love','love',
'/pause','pause',
'/station/(.*)/(.*)','station',
'/volume/(.*)','volume',
'/ban','ban'
)
#create a renderer
template_dir = 'templates'
render = web.template.render(template_dir)
#create the app
app = web.application(urls, globals(),False)
def process_socket(command,get_return=False):
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(3)
#connect to the socket
try:
sock.connect('/home/YOUR_NAME/.shell-fm/unix_file')
#format and send data
sock.sendall( command )
except:
#probably no socket, behave accordingly
return "could not connect to socket"
#get some data back
if get_return:
try:
(string, address)=sock.recvfrom(512)
sock.close()
return string
except:
return "error"
#we need to send commands to
class index:
def GET(self):
return render.index()
class get_track_info:
def GET(self):
command ="info %a::%t::%l::%d::%R::%s::%v::%r\n"
data = process_socket(command,True)
return data
class skip:
def GET(self):
process_socket("skip\n")
return "OK"
class love:
def GET(self):
process_socket("love\n")
class ban:
def GET(self):
process_socket("ban\n")
class pause:
def GET(self):
process_socket("pause\n")
class volume:
def GET(self,direction):
for i in range(3):
process_socket("volume-%s\n" % (direction) )
class station:
def GET(self,type,text):
command = "play lastfm://%s/%s\n" % (type,text)
data = process_socket(command)
return data
if __name__ == "__main__":
app.run()
else:
application = app.wsgifunc()
Go to line 28 and change sock.connect('/home/YOUR_NAME/.shell-fm/unix_file') to point to your unix file that you set up in the shell-fm.rc file.
Almost there, now we just need to create an AJAXy interface.....
Step 4
Creating a web app with pyjamas is quite a bit like creating a Python application with any graphical toolkit, except the code needs to be compiled by Pyjamas and the finished product is HTML and JavaScript.
So go get the pyjamas source. You don't really need to install pyjamas, but you will need to "bootstrap" it. cd
into the pyjamas directory and run "python bootstrap.py" which will create the pyjamas compilers in the bin directory of the pyjamas source directory.
interface.py
from pyjamas.ui.RootPanel import RootPanel
from pyjamas.ui.SimplePanel import SimplePanel
from pyjamas.ui.HorizontalPanel import HorizontalPanel
from pyjamas.ui.Label import Label
from pyjamas.ui.Button import Button
from pyjamas.ui.VerticalPanel import VerticalPanel
from pyjamas.HTTPRequest import HTTPRequest
from pyjamas.Timer import Timer
from pyjamas.ui.ListBox import ListBox
from pyjamas.ui.TextBox import TextBox
from pyjamas.ui.FlexTable import FlexTable
from pyjamas import DOM
from pyjamas import Window
class InfoHandler:
def __init__(self, panel):
self.panel = panel
def onError(self, text, code):
self.panel.onError(text, code)
def onTimeout(self, text):
self.panel.onTimeout(text)
class NullInfoHandler(InfoHandler):
def __init__(self, panel):
InfoHandler.__init__(panel)
self.panel = panel
def onCompletion(self, text):
pass
class ButtonInfoHandler(InfoHandler):
def __init__(self, panel,button):
InfoHandler.__init__(panel)
self.panel = panel
self.button = button
def onCompletion(self, text):
self.button.setEnabled(True)
class TrackInfoHandler(InfoHandler):
def __init__(self, panel):
InfoHandler.__init__(panel)
self.panel = panel
def onCompletion(self, text):
#return the text to the application for processing
self.panel.process_track_info(text)
class Application:
def __init__(self):
#set some vars
self.title = "PyWebShellFM"
#this is where we build the ui
self.statusPanel = VerticalPanel()
self.statusPanel.setID('status_panel')
#make a few Labels to hold artist, track, album info
self.artistLabel = Label()
self.trackLabel = Label()
self.albumLabel = Label()
self.timeLabel = Label()
self.infoTable = FlexTable()
i=0
self.infoTable.setWidget(i,0,Label("Artist:") )
self.infoTable.setWidget(i,1,self.artistLabel)
i+=1
self.infoTable.setWidget(i,0,Label("Track:") )
self.infoTable.setWidget(i,1,self.trackLabel)
i+=1
self.infoTable.setWidget(i,0,Label("Album:") )
self.infoTable.setWidget(i,1,self.albumLabel)
self.statusPanel.add(self.infoTable)
self.statusPanel.add(self.timeLabel)
#make the time bar
timebarWrapperPanel = SimplePanel()
timebarWrapperPanel.setID("timebar_wrapper")
#timebarWrapperPanel.setStyleName('timebar_wrapper')
self.timebarPanel = SimplePanel()
self.timebarPanel.setID("timebar")
#self.timebarPanel.setStyleName('timebar')
timebarWrapperPanel.add(self.timebarPanel)
self.statusPanel.add(timebarWrapperPanel)
#make some shit for buttons
self.buttonHPanel = HorizontalPanel()
self.skipButton = Button("Skip", self.clicked_skip )
self.buttonHPanel.add(self.skipButton)
loveButton = Button("Love", self.clicked_love )
self.buttonHPanel.add(loveButton)
pauseButton = Button("Pause", self.clicked_pause )
self.buttonHPanel.add(pauseButton)
banButton = Button("Ban", self.clicked_ban )
self.buttonHPanel.add(banButton)
#control the volume
self.volumePanel = VerticalPanel()
self.volumeLabel = Label("Volume:")
self.volumePanel.add(self.volumeLabel)
volupButton = Button("volume +", self.clicked_volume_up, 5)
self.volumePanel.add(volupButton)
voldownButton = Button("volume -", self.clicked_volume_down, 5)
self.volumePanel.add(voldownButton)
#make some stuff for station Identification
self.stationInfoHPanel = HorizontalPanel()
stationText = Label("Station: ")
self.stationLabel = Label()
self.stationInfoHPanel.add(stationText)
self.stationInfoHPanel.add(self.stationLabel)
#make buttons and shit to create a new station
self.setStationHPanel = HorizontalPanel()
self.setStationTypeListBox = ListBox()
self.setStationTypeListBox.setVisibleItemCount(0)
self.setStationTypeListBox.addItem("Artist","artist")
self.setStationTypeListBox.addItem("Tags","globaltags")
self.setStationHPanel.add(self.setStationTypeListBox)
self.setStationTextBox = TextBox()
self.setStationTextBox.setVisibleLength(10)
self.setStationTextBox.setMaxLength(50)
self.setStationHPanel.add(self.setStationTextBox)
self.setStationButton = Button("Play", self.clicked_set_station)
self.setStationHPanel.add(self.setStationButton)
#make an error place to display data
self.infoHTML = HTML()
RootPanel().add(self.statusPanel)
RootPanel().add(self.buttonHPanel)
RootPanel().add(self.volumePanel)
RootPanel().add(self.stationInfoHPanel)
RootPanel().add(self.setStationHPanel)
RootPanel().add(self.infoHTML)
def run(self):
self.get_track_info()
def get_track_info(self):
HTTPRequest().asyncGet("/track_info", TrackInfoHandler(self))
Timer(5000,self.get_track_info)
def clicked_skip(self):
self.skipButton.setEnabled(False)
HTTPRequest().asyncGet("/skip",ButtonInfoHandler(self,self.skipButton) )
def clicked_volume_down(self):
HTTPRequest().asyncGet("/volume/down",NullInfoHandler(self) )
def clicked_volume_up(self):
HTTPRequest().asyncGet("/volume/up",NullInfoHandler(self) )
def clicked_love(self):
HTTPRequest().asyncGet("/love",NullInfoHandler(self) )
def clicked_ban(self):
result = Window.confirm("Really ban this song?")
if result:
HTTPRequest().asyncGet("/ban",NullInfoHandler(self) )
def clicked_pause(self):
HTTPRequest().asyncGet("/pause",NullInfoHandler(self) )
def clicked_set_station(self):
type = self.setStationTypeListBox.getSelectedValues()[0]
text = self.setStationTextBox.getText().strip()
if len(text) > 0 :
#clear the text
self.setStationTextBox.setText("")
HTTPRequest().asyncGet("/station/%s/%s"% (type,text),NullInfoHandler(self) )
def set_error(self, content):
self.infoHTML.setHTML("<pre>%s</pre>" % content)
def onTimeout(self,text):
self.infoHTML.setHTML("timeout: "+text)
def onError(self, text, code):
self.infoHTML.setHTML(text + "<br />" + str(code))
def process_track_info(self,text):
#explode the text at ::
#%a::%t::%l::%d::%R::%s::%v::%r
data = text.split("::")
artist = data[0]
track = data[1]
album = data[2]
duration = data[3]
played = int(duration)-int(data[4])
percent = int( played/int(duration)*100 )
self.artistLabel.setText( artist )
self.trackLabel.setText( track )
self.albumLabel.setText( album )
#format time
t = "%s : %d %d" % (duration,played,percent)
self.timeLabel.setText(data[7])
#update the timebarwidth
self.timebarPanel.setWidth("%d%" % (percent) )
#set the station
self.stationLabel.setText(data[5])
#display the volume
self.volumeLabel.setText("Volume: "+data[6])
Window.setTitle(self.title+": "+artist+" - "+track)
if __name__ == '__main__':
app = Application()
app.run()
Compile this code using "/PATH/TO/PYJAMAS/SOURCE/bin/pyjsbuild interface.py". Compiling will create a directory named "output". Copy the contents of "output" into "static/pyjamas"
Now create a style sheet for the progress bar in our interface
static/style/style.css
border: 2px solid #ddd;
width:200px;
height:12px;
}
#timebar{
background-color:black;
height:12px;
}
finally, create the index template
templates/index.html
<head>
<meta name="pygwt:module" content="/static/pyjamas/interface">
<link REL=STYLESHEET type='text/css' href="/static/styles/style.css">
<title>PyWebShellFM</title>
</head>
<body bgcolor="white">
<script language="javascript" src="/static/pyjamas/bootstrap.js"></script>
<iframe id='__pygwt_historyFrame' style='width:0;height:0;border:0'></iframe>
</body>
</html>
Now if everything went according to plan.....
run "shell-fm" to start the shell-fm player
run "python index.py" to start the server
Point your web browser to http://localhost:8080 and enjoy the ugly web interface that is nowhere near "feature complete", but it is a start.
Now quit reading, and go listen to Basil Poledouris
All of these files can be downloaded from http://www.jezra.net/downloads/blogcode/pywebshellfm.tgz
EDIT: What does it look like?
How about a screenshot of the UI, and a picture of the UI in the N810 and the Freerunner.
http://jrobb.org/xfer/code/pywebshellfm/
I'm no jez and I also know zip about python, and wasn't sure of how to get the username in there, so i just made a variable up at the top.
;-)
works alright tho