Dancing the Haka

Dec 2, 2011 / By Yanick Champoux

Tags: , ,

My colleages and I want to set up a LAN radio station, so that we can all groove to the same soundtrack.

To make things interesting, we want to be able to dynamically add songs to the playlist. From any machine.

And since I don’t really have time to do something like that, I’m setting myself a deadline of one evening to get it running.

Got it? Good. For it’s time to rip our shirts. And dance the Haka.

Basic design

Our app will have one ‘collection‘ directory that is going to contain all mp3s.

It will also have a ‘playlist‘ directory. Each file of that directory is going to contain the path to a song to play and they are going to be played in the alphanumerical order of their filenames.

And that’s it.

Choose a streaming server

Of course, it’d be insane to re-implement a streaming server from scratch. So I looked at the offerings out there and settled on icecast2. Under Ubuntu, the server installs without any itch, and the default configuration is Good Enough(tm) for what I need. Excellent.

But the icecast2 server also needs the streamer for our mp3s. This is going to be taken care of by ices0. That one has to be compiled manually, but it’s no big hardship.

Reading the playlist

ices0 can get its playlist different ways. One of them, ah AH, is via a Perl script. So let’s leverage that:

# source abridged for blog entry,
# see GitHub repo for the whole thing

use strict;
use warnings;

use autodie;
use Path::Class;

our $collection_dir = dir( $ENV{COLLECTION} )
    or die 'environment variable COLLECTION not set';

our $playlist_dir = dir( $ENV{PLAYLIST} )
    or die 'environment variable PLAYLIST not set';

sub ices_get_next {
	print "Perl subsystem quering for new track:\n";

    if ( my ( $song ) = sort $playlist_dir->children ) {
        print "playlist is present";
        my $file = file( $song )->slurp( chomp => 1 );
        unlink $song;
        return $file;
    }

    print "playlist empty, get one from the collection";
    my @files = grep { /\.mp3$/ } $collection_dir->children;
    return $files[ rand @files ];
}

Nothing too fancy there. We just pick the first entry in the playlist (and delete it so that we don’t pick it next time) or if there is no playlist items remaining, get a random song from the collection.

Wrapping it in a web app

If we only wanted the streaming server, we’d be done. But we also want peeps to submit new songs. For that, a web application is the best no fuss no muss approach. And since we’re talking of the Haka here, you just know our framework has to be Dancer.

Since we’re going to go have a web app, why not have it deal with the setting and termination of the ices0 streamer?

package Haka;

use 5.10.0;

use FindBin qw($Bin);
use XML::Writer;
use Path::Class;

our $ices_pid = start_ices();
END { kill 1, $ices_pid if $ices_pid; }  # leave no child behind

# defaults
config->{icecast2}{hostname} //= 'localhost';
config->{icecast2}{procotol} //= 'http';
config->{icecast2}{port} //= 8000;

sub start_ices {
    my $ices_conf = "$Bin/../ices/ices.conf";

    open my $conf_fh, '>', $ices_conf;

    my $conf = XML::Writer->new(
        OUTPUT => $conf_fh,
        NAMESPACES => 1,
        NEWLINES => 1
    );

    $conf->startTag( [ 'http://www.icecast.org/projects/ices' =>

            'Configuration' ] );

    $conf->startTag( 'Playlist' );
    $conf->dataElement( 'Randomize' => 0 );
    $conf->dataElement( 'Type' => 'perl' );
    $conf->dataElement( 'Module' => 'ices' );
    $conf->dataElement( 'Crossfade' => 5 );
    $conf->endTag;

    $conf->startTag( 'Execution' );
    $conf->dataElement( 'Background' => 0 );
    $conf->dataElement( 'Verbose' => 0 );
    $conf->dataElement( 'BaseDirectory' => "$Bin/../ices" );
    $conf->endTag;

    my $config = config->{icecast2};

    $conf->startTag( 'Stream' );
    $conf->startTag( 'Server' );
    $conf->dataElement( 'Hostname' => $config->{hostname} || 'localhost' );
    $conf->dataElement( 'Port' => $config->{port} || 8000 );
    $conf->dataElement( 'Password' => $config->{password} );
    $conf->dataElement( 'Protocol' => $config->{procotol} || 'http' );
    $conf->endTag;

    $conf->dataElement( 'Mountpoint' => '/haka' );
    $conf->dataElement( 'Name' => 'Haka' );
    $conf->dataElement( 'Genre' => 'Indus-Techno-Trash' );
    $conf->dataElement( 'Description' => '' );
    $conf->dataElement( 'URL' => '' );
    $conf->dataElement( 'Public' => 0 );

    $conf->dataElement( 'Bitrate' => 128 );
    $conf->dataElement( 'Reencode' => 0 );
    $conf->dataElement( 'Samplerate' => 44_100 );
    $conf->dataElement( 'Channels' => 2 );

    $conf->endTag;
    $conf->endTag;

    $conf->end;

    close $conf_fh;

    if ( my $pid = fork ) {
        return $pid;
    }

    $ENV{COLLECTION} = "$Bin/../collection";
    $ENV{PLAYLIST} = "$Bin/../playlist";
    $ENV{PERL5LIB} = "$Bin/../lib";

    exec 'ices', '-c' => $ices_conf;
}

true;

There we go. Our app now creates the ices0 config file out of its own configuration and launches the streamer as a forked process that is going to be reaped as we eventually exit. All nice and encapsulated.

The web application proper

For this first iteration of the project, we don’t need much:

  • The index page ‘/‘, redirecting to the icecast stream.
  • a ‘/collection‘ page to push new songs to the collection (and add then to the playlist).
  • a ‘/collection/*song*‘ page to check if a song is in the collection.
  • a ‘/playlist‘ page to add an already available song to the playlist.

And that is done via

get '/' => sub {
    state $url = config->{icecast2}{procotol} . '://'
               . config->{icecast2}{hostname}
               . ':' . config->{icecast2}{port}
               . config->{icecast2}{station};

    redirect $url;
};

get '/collection/*' => sub {
    my $song = file( $Bin, '..', 'collection', splat );

    status $song->stat ? 200 : 404;
};

post '/playlist' => sub {
    add_to_playlist( param( 'song' ) );
};

post '/collection' => sub {
    my $song = upload( 'song' );

    my $dest = file( $Bin, '..', 'collection', $song->basename );

    $song->copy_to( $dest->absolute );

    add_to_playlist( $dest->absolute );

    'yay';
};

sub add_to_playlist {
    my $song = shift or return;

    # yes, you are permitted to scream
    my $p = dir( $Bin, '..', 'playlist' );

    print { $p->file( sprintf "%06d",
        10 + max map { file($_)->basename } $p->children
    )->openw } "$song";
}

Client to upload songs

Having a web form to upload the songs one by one would be onerous. A little client script would be much better. Something with which you could do

$ haka_add.pl http://localhost:3000 song1.mp3 song2.mp3 ...

It would be even better if that utility would check if songs are already present on the server and, if so, only add them to the playlist.

use 5.10.0;

use strict;
use warnings;

use LWP::UserAgent;
use Path::Class;

my ( $haka_home, @songs ) = @ARGV;

my $agent = LWP::UserAgent->new;

while ( my $song = shift @songs ) {
    $song = file( $song );
    print "adding '$song' to haka...";

    if ( $agent->get( "$haka_home/collection/".$song->basename )->is_success ) {
        say "song already present, no need to upload";
        $agent->post( "$haka_home/playlist", {
            song => $song->basename
        }
        )->is_success or die "adding to playlist failed";
    }
    else {
        $agent->post( "$haka_home/collection",
            Content_Type => 'form-data',
            Content      => [
                song => [ ''.$song->relative ],
            ],
        )->is_success or die "upload failed";
    }
    say 'done';
}

That wasn’t so hard, was it?

Waewae takahia kia kino!

And there we are. It’s a raw app, with no fancy interface. But it’s a working, dynamic, shared radio station. Hacked together in 3 hours.

Can we say T?nei te tangata p?huruhuru?

As usual, if you want to play and experiment, the code is on Github.

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>