Adding a Web Interface to LiquidSoap

From JoatWiki
Jump to: navigation, search

(Work in progress, please come back later...)

Contents

Intro

One of my nasty habits is taking other peoples' code, dipping it in Perl, and wrapping a web interface around it. Doing such with LiquidSoap is easy because it comes with a telnet interface. Because Perl already has a telnet module, all you need to add is the parser. The trick is making it useful. Hopefully you'll build on what I've done so far.

First off, the disclaimers: This is unsecure code. It is not intended for public-facing services. I use it inside of my home network where only two computers can access it. My apologies if my code looks bad or is confusing. I'm not a professional programmer, so I don't get paid to make it look good or run securely. Hopefully it'll give ideas of your own though.

Next, a bit about Savonet and LiquidSoap:

  • Savonet is a set of open source tools whose purpose is to generate/handle audio streams. It is written mostly in OCaml, a programming language that has been around, in one form or another, since the mid-80's.
  • Liquidsoap is Savonet's scripting language to manage those tools and "glue" them together, much in the same way that Perl handles text. Some of the more unique features is the ability to transcode and/or mix streams on the fly (we'll come back to this). Please visit the site for a much longer list of specific features (remember, Savonet and Liquidsoap are still in development).

And a bit about why I've done what I've done: I hate web programs that require multiple scripts to run. With the exception of MediaWiki (what this wiki runs on), I tend to use single purpose tools. Thus, with me you get oddly recursive scripts. Show me a better way and I'll start using it.

As an aid, I will attempt to lead you through my work, step by step.

Basic thoughts about design

I wanted a tool which could push an audio stream (live or MP3's from a library) to one or more in-house players via an Icecast or Shoutcast server. As a result of years of gadget buying, I have a number of older devices which function as networked audio (Linksys NSLU2's, a D-Link DSM-320, a Zipit) and video (MediaMVP) interfaces, not to mention my wife's and son's computers.

In recent experiments, I've successfully (and sometimes, semi-successfully) hooked together a number of programs including: MPD, SlimServer, Cidero, Icecast, Ices, Shoutcast, SageTV, MythTV, and Asterisk. Because most of them handle common formats, there's usually not a whole of of issue in getting them to work together.

Language choice

The problem with those programs is that their strengths are also their weakness. While I recognize the power in hooking things together and am willing to put in a few minutes of "set up work", my wife just wants to hit the power button, pick songs or playlists, and expects them to play immediately. Thus the need for a simplified interface, easiest of which is the web interface. I'm a Perl geek, so you get a Perl-based CGI script. This could probably be written in any other language just as easily, but since Liquidsoap has a text (telnet) interface, it's likely to be easier in Perl.

Initial set up (loading the modules)

We'll want to store information about the music (storage paths, playlists, etc.) in a database. For that I'll use MySQL. Since it's Perl, using the Telnet and DBI (MySQL) modules, the top of the script will look like:

#!/usr/bin/perl

use CGI;
use DBI;
use Net::Telnet;

$query=new CGI;
$t=new Net::Telnet (Timeout => 10, Port => "1234",Prompt => '/END$/');

$sql_database_name="music";
$sql_database_host="localhost";
$sql_database_port="3306";
$sql_connect_string="DBI:mysql:$sql_database_name:$sql_database_host:$sql_databa
se_port";
$sql_username="jukeuser";
$sql_password="jukepass";
$binpath="/~joat/cgi-bin/player.cgi";

The lines starting with "use" tell Perl to load those specific modules. (Note: you may have to install them first.) Don't worry about having to type in all of this, I'll post a link to the code once it gets to the useable stage. The "new CGI" and "new Net::Telnet" lines initiate instances of those modules.

Grabbing variables from CGI

The following extract various variables from the CGI input:

$action=$query->param("action");
chomp $action;
$id=$query->param("id");
chomp $id;
$songtitle=$query->param("songtitle");
chomp $songtitle;
$artist=$query->param("artist");
chomp $artist;
$path=$query->param("path");
chomp $path;
$classical=$query->param("classical");
chomp $classical;
$sleepy=$query->param("sleepy");
chomp $sleepy;
$techno=$query->param("techno");
chomp $techno;
$bumper=$query->param("bumper");
chomp $bumper;
$humorous=$query->param("humorous");
chomp $humorous;
$favorite=$query->param("favorite");
chomp $favorite;
$opentune=$query->param("opentune");
chomp $opentune;
$sortby=$query->param("sortby");
chomp $sortby;
$genre=$query->param("genre");
chomp $genre;

Database subroutines

Notice that I didn't use "new DBI" in the above. For MySQL access, I use a number of subroutines because my scripts tend to be database intensive. In the long run, they save on key strokes and make the scripts easier to read. Here's my database subroutines (I tend to put these at the end of a script):

# connect to database
sub connect_to_database {
         $dbh = DBI->connect($sql_connect_string,$sql_username,$sql_password) or \
         die "Connecting : $DBI::errstr\n ";
}

# disconnect from database
sub disconnect_from_database {
        $dbh->disconnect();
}

# process the query
sub process_sql {
        my $sqlstring=$_[0];
        $sth = $dbh->prepare("$sqlstring") or die "preparing: ",$dbh->errstr;
        $sth->execute or die "executing: ", $dbh->errstr;
}

Database population

Next, you want to create a database with a few entries in it so you have something to mess with while your working. As root, run "mysql" and create the database via:

 create database music;

Then "use music" and create the songs table via:

 use music;
 create table songs (
 id int not null auto_increment primary key,
 songtitle varchar(255) not null,
 artist varchar(255) not null,
 path varchar(255) not null,
 favorite varchar(1) not null,
 opentune varchar(1) not null,
 classical varchar(1) not null,
 sleepy varchar(1) not null,
 bumper varchar(1) not null,
 techno varchar(1) not null,
 humorous varchar(1) not null);

Note that those aren't really genres of music. They're just categories that I picked for various of my own listening styles. You can add in the official genres later.

Add a couple of your songs to the database via;

 insert into songs(songtitle,artist,path) values("Violin Concerto","Devin Anderson","/home/joat/Desktop/music/radio/da_violin_concerto.mp3");
 insert into songs(songtitle,artist,path) values("Ambulance Ride for the Soon to be Departed","Devin Anderson","/home/joat/Desktop/music/radio/arftstbd.mp3");
 insert into songs(songtitle,artist,path) values("Only Hope","Tongue of the Dog","/home/joat/Desktop/music/radio/only_hope.mp3");

Check that you now have records in the database via:

 select * from songs;

The main page

I used a set of frames to provide a basic lay out. I wanted the currently playing song shown at the top. Below that, I wanted three columns containing a basic menu, song choices, and a display of queued up songs. To get this, the default function is:

 sub print_main_page() {
        print "Content-type: text/html\n\n";
        print "<html><head><title>Joat's Jukebox</title></head>";
        print "<frameset cols=\"130,*\">";
        print "<frame src=\"$binpath?action=menu\" name=\"left\">";
        print "<frameset rows=\"6%,94%\">";
        print "<frame src=\"$binpath?action=OnAir\" name=\"ONAIR\">";
        print "<frameset cols=\"65%,35%\">";
        print "<frame src=\"$binpath?action=Songs\" name=\"SONGS\">";
        print "<frame src=\"$binpath?action=Queue\" name=\"QUEUE\">";
        print "</frameset>";
        print "</frameset>";
        print "</frameset>";
        print "</html>";
 }

The above loads the main frame set and recursive calls our script with different actions. The "name=" declaration in the above just sets the name for the frame. You can call it "LEFT", "MIDDLE" and "RIGHT" (or anything else) if you want to. To capture the action, near the top of the script we use:

 $action=$query->param("action");
 chomp $action;

From there, we can perform specific actions based on the value in the variable "$action". If we want to load the left-hand menu, we use:

 if ($action eq "menu") {
        print_page_header();
        print "<p><a href=\"$binpath?action=Queue\" target=\"QUEUE\">Queue</a><br/>";
        print "<a href=\"$binpath?action=Songs\" target=\"SONGS\">Songs</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=favorites\" target=\"SONGS\">- Favorites</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=open\" target=\"SONGS\">- Open</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=classical\" target=\"SONGS\">- Classical</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=sleepy\" target=\"SONGS\">- Sleepy</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=techno\" target=\"SONGS\">- Techno</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=humorous\" target=\"SONGS\">- Humourous</a><br/>";
        print "<a href=\"$binpath?action=Songs&genre=bumper\" target=\"SONGS\">- Bumpers</a><br/>";
        print "<p><a href=\"http://192.168.1.79:8000/listen.mp3\">listen</a>";
        print_page_footer();
 }

Note the "target=" declaration in the above. This is where those "name=" values from the main frame set come into play. Also note the line just above "print_page_footer()". That's just a link to listen to my in-house Icecast server.

Almost forgot... Each sub-frame is a web page in itself. From Perl, you need to issue a page header via:

sub print_page_header() {
       my $headdata=$_[0];
       chomp $headdata;
       print "Content-type: text/html\n\n";
       print "";
        print "";
        print "$headdata";
        print "";
        print "";
 }

The stuff in between the style tags are used to make cosmetic adjustments to what is shown within the frames.  The $headdata variable is there to add in tags such as one to automatically refresh the page periodically.

To make things look pretty, we'll do something similar with the closing syntax for the page:
 sub print_page_footer() {
        print "";
}

Liquidsoap

Note: the code in this section no longer works due to an upgrade in the language. Rewrite coming soon.

At some point during your experimenting, you'll want to actually test your code. This means that you have to be running Liquidsoap in the background. Here's a copy of my "lsplayer.liq" script (note: the following doesn't work due to a couple tweaks that I've made. I'll fix it shortly):

#!/usr/local/bin/liquidsoap

set log.stdout = true
set log.dir = "/tmp"
set telnet = true

numbers = mksafe(playlist("/home/joat/Desktop/music/fallback.m3u"))
numbers = fallback([normalize(request.queue(id="q")),numbers])

# the normal playlist
normal = mksafe(playlist("/home/joat/Desktop/music/mylist.m3u"))

# And now the magic :)
def smooth_add(~normal,~special)
  d = 1.  # delay before mixing after beginning of mix
  p = 0.2 # portion of normal when mixed
  fade.final = fade.final(duration=d*.2.)
  fade.initial = fade.initial(duration=d*.2.)
  q = 1. -. p
  # We alias change_volume to c
  c = change_volume
  fallback(track_sensitive=false,
           [special,normal],
           transitions=[
             fun(normal,special)->
               add(normalize=false,[sequence([blank(duration=d),c(q,special)]),
                                    c(q,fade.final(normal)),
                                    c(p,normal)]),
             fun(special,normal)->
               add(normalize=false,[c(p,normal),c(q,fade.initial(normal))])
           ])
end

# output to an in-house icecast server 
output.icecast.mp3(host="192.168.1.79",password="hackme",mount="listen.mp3",port=8000,\
  name="DJjoat",description="requests at http://www.757.org/~joat/request.php",mksafe(numbers))
# as an experiment the following will accept a stream from a Slimserver source and forward it to the Icecast server
#output.icecast.mp3(host="192.168.1.79",password="hackme",mount="listen.mp3",port=8000,\
  name="DJjoat",description="requests at http://www.757.org/~joat/request.php",\
  mksafe(input.http("http://192.168.1.175:9000/stream.mp3")))

Note: the above is a separate script, with a ".liq" suffix. That content should not be put into the perl code. The above sets up a Liquidsoap daemon which you can access by telneting to port 1234 on the machine where the script is run.

To access the above script, we'll add the following to the CGI script:

sub liq {
       my $liqcmd=$_[0];
       $t->open("localhost");
       @lines= $t->cmd("$liqcmd");
       $t->close("localhost");
       return @lines;
}



Sources: - http://www.databasejournal.com/features/mysql/article.php/10897_3434641_2



<comments>Adding_a_Web_Interface_to_LiquidSoap</comments>

Personal tools