2011-03-18

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:

username = YOUR_LASTFM_USERNAME
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 the web module
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(urlsglobals(),False)

def process_socket(command,get_return=False):
  sock socket.socket(socket.AF_UNIXsocket.SOCK_STREAM)
  sock.settimeout(3)
  #connect to the socket
  try:  
    sock.connect('/home/YOUR_NAME/.shell-fm/unix_file')  
    #format and send data
    sock.sendallcommand )
  except:
    #probably no socket, behave accordingly
    return "could not connect to socket"
    
  #get some data back
  if get_return:
    try:
      (stringaddress)=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 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.HTML import HTML
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__(selfpanel):
    self.panel panel 
  
  def onError(selftextcode):
    self.panel.onError(textcode)

  def onTimeout(selftext):
    self.panel.onTimeout(text)
    
class NullInfoHandler(InfoHandler):
  def __init__(selfpanel):
    InfoHandler.__init__(panel)
    self.panel panel

  def onCompletion(selftext):
    pass  

class ButtonInfoHandler(InfoHandler):
  def __init__(selfpanel,button):
    InfoHandler.__init__(panel)
    self.panel panel
    self.button button
    
  def onCompletion(selftext):
    self.button.setEnabled(True)

class TrackInfoHandler(InfoHandler):
  def __init__(selfpanel):
    InfoHandler.__init__(panel)
    self.panel panel

  def onCompletion(selftext):
    #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_up5)
    self.volumePanel.add(volupButton)
    voldownButton Button("volume -"self.clicked_volume_down5)
    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) > :
      #clear the text
      self.setStationTextBox.setText("")
      HTTPRequest().asyncGet("/station/%s/%s"% (type,text),NullInfoHandler(self) )

  def set_error(selfcontent):
    self.infoHTML.setHTML("<pre>%s</pre>" content)
    
  def onTimeout(self,text):
    self.infoHTML.setHTML("timeout: "+text)
    
  def onError(selftextcode):
    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 intplayed/int(duration)*100 )
    self.artistLabel.setTextartist )
    self.trackLabel.setTexttrack )
    self.albumLabel.setTextalbum )
    #format time
    "%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

#timebar_wrapper {
  border2px solid #ddd;
  width:200px;
  height:12px;
}
#timebar{
  background-color:black;
  height:12px;
}

finally, create the index template
templates/index.html

<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.

Comments
2011-03-19 Egil:
Some screenshots of the interface would be awesome.
2011-03-19 jezra:
Yea, that's a good idea.
2011-03-20 jrobb:
100% awesome
2011-03-22 PeteV:
total awesomeness, makes me want to tinker more too.
2011-03-22 jezra:
always tinker. ;)
2011-03-27 jrobb:
I use the recommended radio sometimes, so I added in another line for personal/playlist/recommended radio stuffs.

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
2011-03-27 jezra:
jrobb, the link to the file isn't working because your server is trying to run the python script.
2011-09-24 chris:
Love it! Please add a license and put it in a public git repo, and I'll try to get it integrated into OpenWRT.
Name:
not required
Email:
not required (will not be displayed)
Website:
not required (will link your name to your site)
Comment:
required
Please do not post HTML code or bbcode unless you want it to show up as code in your post. (or if you are a blog spammer, in which case, you probably aren't reading this anyway).
Prove you are human by solving a math problem! I'm sorry, but due to an increase of blog spam, I've had to implement a CAPTCHA.
Problem:
1 minus 1
Answer:
required
  • Tags:
  • Python
subscribe
 
2019
2016
2015
2014
2013
2012
2011
2010
December
November
October
September
August
July
June
May
April
March
February
January
2009
December
November
October
September
August
July
June
May
April
March
February
January
2008