#!/usr/bin/perl -w
#
# $Id: musicd,v 1.17 2002/03/03 22:21:07 mdb Exp $
#
# MP3 audio server. Manages playlists of mp3 files that live on a local
# filesystem. The playlists can be manipulated by connecting to the server
# on a port and issuing simple text commands.

use strict;
use IO::Socket;
use IO::Select;
use MPEG::MP3Play ':state';
use Data::Dumper;
use Getopt::Long;

my $usage = "Usage: $0 [--bind_addr x.x.x.x]\n";

# this is what's currently being played
my $playing;
# this is our primary playlist
my @playlist;
# this is our current index into the playlist
my $index = -1;
# this is our handle on the MP3 player object
my $mp3;
# whether or not we're currently paused
my $paused = 0;

# get our options
my $bind_addr; # bind address
GetOptions("bind_addr:s" => \$bind_addr);

# validate the bind address
die $usage if (defined $bind_addr && $bind_addr !~ /\d+\.\d+\.\d+\.\d+/);

main: {
    # create the server part of things
    my $sock;
    if (defined $bind_addr) {
        $sock = new IO::Socket::INET(LocalHost => "$bind_addr:2500",
                                     Proto => "tcp",
                                     Listen => SOMAXCONN,
                                     Reuse => 1);
    } else {
        $sock = new IO::Socket::INET(LocalPort => 2500,
                                     Proto => "tcp",
                                     Listen => SOMAXCONN,
                                     Reuse => 1);
    }
    $sock or die "Unable to create server socket: $!\n";

    # create our MP3 interface
    $mp3 = new MPEG::MP3Play;
    $mp3->set_player_mode(&XA_PLAYER_MODE_OUTPUT_AUTO_CLOSE_ON_STOP);

    # manage our various inputs
    my $sel = new IO::Select($sock);
    my $mph = $mp3->get_command_read_pipe();
    $sel->add($mph);

    # do our main loop thang
    my (@ready, $fh);
    while (@ready = $sel->can_read()) {
        foreach $fh (@ready) {
            if ($fh == $mph) {
                $mp3->process_messages_nowait();

            } elsif ($fh == $sock) {
                $sel->add($sock->accept());

            } else {
                # if handle_client() returns false, we remove this client from
                # the select list and close the socket
                if (!handle_client($fh)) {
                    $sel->remove($fh);
                    $fh->close();
                }
            }
        }
    }
}

# handles input from a client
sub handle_client
{
    my ($cfh) = @_;

    # see what the client has to say
    my $input = <$cfh>;

    if (defined $input) {
        # remove trailing cr/nl
        $input =~ s:[\r\n]+$::g;
    } else {
        # bail out if the socket is closed
        return 0;
    }

    # parse and process the command
    if ($input =~ m/^APPEND (\d+) (\d+) (.*)/) {
        # append this song to the playlist
        my $eid = $1;
        my $sid = $2;
        my $path = $3;

        # make sure the file they provided exists and is readable
#        if (-r $path) {
            push @playlist, [ $eid, $sid, $path ];
            print $cfh "200 Appended '$eid:$sid:$path' to playlist.\n";
#        } else {
#            print $cfh "404 Unable to read playlist candidate: '$path'\n";
#        }

        # if we're not playing a song, start playing now that we have a
        # playlist with songs in it
        if (!defined $playing) {
            play_next_tune(0);
        }

    } elsif ($input =~ m/^REMOVEGRP (\d+) (\d+)/) {
        my $sid = $1;
        my $count = $2;

        # remove the requested number of songs from the playlist starting
        # with the first song that matches the specified songid
        my $sidx = -1;
        my $i;
        my $yanked = 0;

        for ($i = 0; $i < @playlist; $i++) {
            if ($playlist[$i]->[1] == $sid) {
                $sidx = $i;
                splice(@playlist, $i, $count);
                # adjust the playing index if necessary
                if ($index < $i) {
                    # nothing to do here
                } elsif ($index >= $i && $index < $i + $count) {
                    # we removed the playing tune
                    $yanked = 1;
                    $index = $i-1;
                } else {
                    # we removed tunes from before the currently playing
                    # tune so we adjust index
                    $index -= $count;
                }
                last;
            }
        }

        # now check to see if we've removed the playing tune
        if ($yanked) {
            play_next_tune(0);
        }

        if ($sidx >= 0) {
            print $cfh "200 Removed $count entries from position $sidx.\n";
        } else {
            print $cfh "404 Unable to locate $sid in playlist.\n";
        }

    } elsif ($input =~ m/^REMOVE (\d+)/) {
        my $sid = $1;

        # if we're playing the song to remove, skip to the next song
        if ($sid == $playing->[1]) {
            play_next_tune(1);
        }

        # remove this songid from the playlist
        my $sidx = -1;
        my $i;

        for ($i = 0; $i < @playlist; $i++) {
            if ($playlist[$i]->[1] == $sid) {
                $sidx = $i;
                splice(@playlist, $i, 1);
                # adjust the playing index if necessary
                $index -= 1 if ($i < $index);
                last;
            }
        }

        if ($sidx >= 0) {
            print $cfh "200 Removed $sid from position $sidx.\n";
        } else {
            print $cfh "404 Unable to locate $sid in playlist.\n";
        }

    } elsif ($input =~ m/^CLEAR/) {
        # clear out the current playlist and reset the playing index
        @playlist = ();
        $index = -1;
        print $cfh "200 Playlist cleared.\n";

    } elsif ($input =~ m/^PLAYLIST/) {
        print $cfh "200 Playlist songs: " . @playlist . " current: " .
            (defined $playing ? $playing->[1] : "<none>") . "\n";
        my $entry;
        foreach $entry (@playlist) {
            print $cfh join("\t", @{$entry}) . "\n";
        }

    } elsif ($input =~ m/^PLAYING/) {
        print $cfh "200 Currently " . ($paused ? "paused" : "playing") . ": " .
            (defined $playing ? $playing->[1] : "<none>") . "\n";

    } elsif ($input =~ m/^STOP/) {
        $mp3->stop();
        $paused = 0; # clear the paused flag
        undef $playing;
        print $cfh "200 Music stopped.\n";

    } elsif ($input =~ m/^PLAY/) {
        # if we've already queued up a song, play it
        if (defined $playing) {
            $mp3->play();
        } else {
            # otherwise queue one up to play
            play_next_tune(0);
        }

        # clear the paused flag
        $paused = 0;

        # and report back based on whether or not we successfully queued
        # up a song for playing
        if (defined $playing) {
            print $cfh "200 Music started.\n";
        } else {
            print $cfh "501 Can't play: playlist empty.\n";
        }

    } elsif ($input =~ m/^PAUSE/) {
        $mp3->pause();
        $paused = 1;
        print $cfh "200 Music paused (use PLAY to unpause).\n";

    } elsif ($input =~ m/^SKIPTO (\d+)/) {
        my $sid = $1;
        # stop this tune and skip to the specified tune
        $mp3->stop();
        if (play_this_tune($sid)) {
            print $cfh "200 Skipped to $sid.\n";
        } else {
            print $cfh "404 Unable to locate song $sid.\n";
        }

    } elsif ($input =~ m/^BACK/) {
        # stop this tune and skip to the previous one
        $mp3->stop();
        play_next_tune(-1);
        print $cfh "200 Skipped current song.\n";

    } elsif ($input =~ m/^SKIP/) {
        # stop this tune and skip to the next one
        $mp3->stop();
        play_next_tune(1);
        print $cfh "200 Skipped current song.\n";

    } elsif ($input =~ m/^CLOSE/) {
        print $cfh "200 Bye bye.\n";
        return 0;

    } elsif ($input =~ m/^EXIT/) {
        print $cfh "200 Bye bye.\n";
        exit(0);

    } else {
        print $cfh "500 Unknown command: '$input'\n";
    }

    return 1;
}

sub play_next_tune
{
    my ($direction) = @_;
    my $pcount = @playlist;

    # bail if the playlist is empty
    if ($pcount == 0) {
        $index = -1;
        undef $playing;
	print "Empty playlist, twiddling.\n";
        return;
    }

    # advance the playlist pointer either forward or backward (wrapping
    # around at the end because we always loop)
    $index = ($index + $pcount + $direction) % $pcount;
    $playing = $playlist[$index];

    # open it
    $mp3->open($playing->[2]);

    # and play it
    $mp3->play();
}

sub play_this_tune
{
    my ($sid) = @_;
    my $i;

    # bail if the playlist is empty
    if (@playlist == 0) {
        $index = -1;
        undef $playing;
        return 0;
    }

    # advance the playlist pointer (wrapping around at the end because we
    # always loop)
    for ($i = 0; $i < @playlist; $i++) {
        $index = ($index + 1) % @playlist;
        $playing = $playlist[$index];
        if ($playing->[1] == $sid) {
            last;
        }
    }

    # open it
    $mp3->open($playing->[2]);

    # and play it
    $mp3->play();

    # let them know if we found the tune
    return ($playing->[1] == $sid);
}

# message handlers
package MPEG::MP3Play;

sub msg_notify_player_state
{
    my ($mp3, $msg) = @_;

    # deeee-bug!
    # print "state: " . Dumper($msg) . "\n";

    # play the next tune if we hit the end of this one
    if ($msg->{state} == &XA_PLAYER_STATE_EOF) {
      main::play_next_tune(1);
    }
    return 1;
}

sub msg_notify_error
{
    my ($mp3, $msg) = @_;
    print "error: " . Dumper($msg) . "\n";
    return 1;
}

sub msg_notify_debug
{
    my ($mp3, $msg) = @_;
    if ($msg->{code} == &XA_ERROR_NO_SUCH_FILE) {
        print "unable to load: $playing\n";
    } else {
        print "debug: " . Dumper($msg) . "\n";
    }
    print "nsf: " . &XA_ERROR_NO_SUCH_FILE . "\n";
    return 1;
}
